summaryrefslogtreecommitdiff
path: root/.config
diff options
context:
space:
mode:
Diffstat (limited to '.config')
-rw-r--r--.config/emacs/custom.el20
-rw-r--r--.config/emacs/early-init.el107
-rw-r--r--.config/emacs/init.el214
-rw-r--r--.config/emacs/modules/mm-abbrev.el46
-rw-r--r--.config/emacs/modules/mm-buffer-menu.el15
-rw-r--r--.config/emacs/modules/mm-calc.el12
-rw-r--r--.config/emacs/modules/mm-completion.el170
-rw-r--r--.config/emacs/modules/mm-darwin.el30
-rw-r--r--.config/emacs/modules/mm-dired.el34
-rw-r--r--.config/emacs/modules/mm-documentation.el46
-rw-r--r--.config/emacs/modules/mm-editing.el369
-rw-r--r--.config/emacs/modules/mm-humanwave.el257
-rw-r--r--.config/emacs/modules/mm-keybindings.el185
-rw-r--r--.config/emacs/modules/mm-projects.el48
-rw-r--r--.config/emacs/modules/mm-search.el26
-rw-r--r--.config/emacs/modules/mm-tetris.el19
-rw-r--r--.config/emacs/modules/mm-theme.el232
-rw-r--r--.config/emacs/modules/mm-treesit.el261
-rw-r--r--.config/emacs/modules/mm-window.el79
-rw-r--r--.config/emacs/site-lisp/editing-functions.el163
-rw-r--r--.config/emacs/site-lisp/gh.el68
-rw-r--r--.config/emacs/site-lisp/grab.el215
-rw-r--r--.config/emacs/site-lisp/highlighter.el132
-rw-r--r--.config/emacs/site-lisp/html-escape.el56
-rw-r--r--.config/emacs/site-lisp/increment.el132
-rw-r--r--.config/emacs/site-lisp/jq.el115
-rw-r--r--.config/emacs/site-lisp/mm-lib.el80
-rw-r--r--.config/emacs/site-lisp/multiple-cursors-extensions.el123
-rw-r--r--.config/emacs/site-lisp/number-format.el129
-rw-r--r--.config/emacs/themes/mango-dark-theme.el250
-rw-r--r--.config/emacs/themes/mango-light-theme.el250
31 files changed, 3883 insertions, 0 deletions
diff --git a/.config/emacs/custom.el b/.config/emacs/custom.el
new file mode 100644
index 0000000..198e881
--- /dev/null
+++ b/.config/emacs/custom.el
@@ -0,0 +1,20 @@
+;;; -*- lexical-binding: t -*-
+(custom-set-variables
+ ;; custom-set-variables was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(package-selected-packages nil)
+ '(package-vc-selected-packages
+ '((vue-ts-mode :url "https://github.com/8uff3r/vue-ts-mode.git"
+ :branch "main" :vc-backend Git)
+ (gsp-ts-mode :url "https://git.thomasvoss.com/gsp-ts-mode"
+ :branch "master" :vc-backend Git)
+ (xcompose-mode :url "https://git.thomasvoss.com/xcompose-mode"
+ :branch "master" :vc-backend Git))))
+(custom-set-faces
+ ;; custom-set-faces was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ )
diff --git a/.config/emacs/early-init.el b/.config/emacs/early-init.el
new file mode 100644
index 0000000..8057db1
--- /dev/null
+++ b/.config/emacs/early-init.el
@@ -0,0 +1,107 @@
+;;; early-init.el --- Emacs early init file -*- lexical-binding: t; -*-
+
+;;; XDG Base Directory Specification Compliance
+
+(eval-when-compile
+ (require 'xdg))
+
+(defconst mm-cache-directory
+ (expand-file-name "emacs-small" (xdg-cache-home))
+ "The XDG-conformant cache directory that Emacs should use.")
+
+(defconst mm-config-directory
+ (expand-file-name "emacs-small" (xdg-config-home))
+ "The XDG-conformant config directory that Emacs should use.")
+
+(defconst mm-data-directory
+ (expand-file-name "emacs-small" (xdg-data-home))
+ "The XDG-conformant data directory that Emacs should use.")
+
+(dolist (directory (list mm-cache-directory
+ mm-config-directory
+ mm-data-directory))
+ (make-directory directory :parents))
+
+(setopt user-emacs-directory mm-cache-directory
+ auto-save-list-file-prefix (expand-file-name
+ "auto-save-list-"
+ mm-cache-directory)
+ backup-directory-alist `(("." . ,(expand-file-name
+ "backups" mm-cache-directory))))
+
+(when (native-comp-available-p)
+ (startup-redirect-eln-cache
+ (expand-file-name (expand-file-name "eln/" mm-cache-directory))))
+
+
+;;; Useful Constants
+
+(defconst mm-darwin-p (eq system-type 'darwin)
+ "This variable is non-nil if Emacs is running on a Darwin system.")
+
+(defconst mm-humanwave-p (file-exists-p "~/.humanwavep")
+ "This variable is non-nil if Emacs is running on a Humanwave system.")
+
+
+;;; Basic Frame Settings
+
+(setopt frame-resize-pixelwise t
+ frame-inhibit-implied-resize t
+ ring-bell-function #'ignore
+ use-short-answers t
+ inhibit-splash-screen t
+ inhibit-startup-buffer-menu t)
+(if mm-darwin-p
+ (progn
+ (add-to-list 'default-frame-alist '(fullscreen . maximized))
+ (when (featurep 'ns)
+ (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))))
+ (add-to-list 'default-frame-alist '(undecorated . t))
+ (menu-bar-mode -1))
+(scroll-bar-mode -1)
+(tool-bar-mode -1)
+
+
+;;; Startup Performance
+
+(setopt gc-cons-threshold most-positive-fixnum
+ gc-cons-percentage 0.5)
+(setopt read-process-output-max
+ (let ((pipe-size-file "/proc/sys/fs/pipe-max-size"))
+ (if (file-exists-p pipe-size-file)
+ (with-temp-buffer
+ (insert-file-contents pipe-size-file)
+ (number-at-point))
+ (* 1024 1024))))
+
+;; Set ‘file-name-handler-alist’ and ‘vc-handled-backends’ to nil
+;; temporarily and restore them once Emacs has properly initialized. We
+;; set threshold to 8 MiB which seems to be a good middleground for now.
+;; A higher threshold means less garbage collections but I’ve had issues
+;; with those garbage collections causing long freezes when they occur.
+(let ((saved-file-name-handler-alist file-name-handler-alist))
+ (setopt file-name-handler-alist nil)
+ (add-hook
+ 'emacs-startup-hook
+ (defun mm-restore-emacs-settings ()
+ (setopt gc-cons-threshold (* 1024 1024 8)
+ gc-cons-percentage 0.1
+ file-name-handler-alist saved-file-name-handler-alist))))
+
+
+;;; Avoid Flashbang
+
+;; (setq-default mode-line-format nil) ; This will be set in init.el
+
+;; Colors taken from ‘mango-theme’
+(let ((background "#2B303B")
+ (foreground "#C5C8C6"))
+ (set-face-attribute
+ 'default nil
+ :background background
+ :foreground foreground)
+ (set-face-attribute
+ 'mode-line nil
+ :background background
+ :foreground foreground
+ :box 'unspecified))
diff --git a/.config/emacs/init.el b/.config/emacs/init.el
new file mode 100644
index 0000000..6e85356
--- /dev/null
+++ b/.config/emacs/init.el
@@ -0,0 +1,214 @@
+;;; init.el --- Main Emacs configuration file -*- lexical-binding: t; -*-
+
+;;; Preamble
+
+;; To inhibit this message you MUST do this in init.el, MUST use ‘setq’,
+;; and MUST write your login name as a string literal. Thanks Emacs!
+;;
+;; The ‘eval’ is required in the case that this file is byte-compiled.
+(if mm-humanwave-p
+ (eval '(setq inhibit-startup-echo-area-message "thomasvoss"))
+ (eval '(setq inhibit-startup-echo-area-message "thomas")))
+
+;; Add custom lisp code into the load path
+(dolist (directory '("." "modules" "site-lisp"))
+ (add-to-list 'load-path (expand-file-name directory mm-config-directory)))
+
+;; Require helpers used by the rest of the config
+(require 'mm-lib)
+
+
+;;; Silent Native Compilation
+
+(when (native-comp-available-p)
+ (setopt
+ native-comp-async-report-warnings-errors nil
+ native-compile-prune-cache t))
+
+
+;;; Package Management
+
+(setopt
+ package-user-dir (expand-file-name "pkg" mm-data-directory)
+ package-gnupghome-dir (or (getenv "GNUPGHOME")
+ (expand-file-name "gnupg" package-user-dir))
+ package-archives (cl-loop with proto = (if (gnutls-available-p) "https" "http")
+ for (name . url) in
+ '(("gnu" . "elpa.gnu.org/packages/")
+ ("melpa" . "melpa.org/packages/")
+ ("nongnu" . "elpa.nongnu.org/nongnu/"))
+ collect (cons name (concat proto "://" url)))
+ package-archive-priorities '(("gnu" . 3)
+ ("nongnu" . 2)
+ ("melpa" . 1)))
+(setopt use-package-always-defer t)
+
+(package-initialize)
+
+(defun mm-package-sync ()
+ "Remove unused packages and install missing ones."
+ (interactive)
+ (let ((window-configuration (current-window-configuration)))
+ (package-autoremove)
+ (package-install-selected-packages)
+ (package-upgrade-all)
+ (package-vc-install-selected-packages)
+ (package-vc-upgrade-all)
+ (set-window-configuration window-configuration))
+ (message "Done syncing packages."))
+
+
+;;; Generic Emacs Configuration
+
+(defvar mm-initial-scratch-message
+ (format
+ (substitute-quotes
+ ";; This is `%s'. Use `%s' to evaluate and print results.\n\n")
+ initial-major-mode
+ (substitute-command-keys
+ "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]"))
+ "The initial message to display in the scratch buffer.")
+
+(use-package emacs
+ :demand t
+ :custom
+ (ad-redefinition-action 'accept)
+ (case-fold-search nil)
+ (create-lockfiles nil)
+ (custom-file (expand-file-name "custom.el" mm-config-directory))
+ (custom-safe-themes t)
+ (delete-pair-blink-delay 0)
+ (disabled-command-function nil)
+ (duplicate-line-final-position -1)
+ (duplicate-region-final-position -1)
+ (echo-keystrokes 0.01) ; 0 disables echoing
+ (echo-keystrokes-help nil)
+ (extended-command-suggest-shorter nil)
+ (initial-buffer-choice nil)
+ (initial-scratch-message mm-initial-scratch-message)
+ (kill-do-not-save-duplicates t)
+ (large-file-warning-threshold nil)
+ (make-backup-files nil)
+ (next-error-recenter '(4)) ; ‘center of window’
+ (read-extended-command-predicate #'command-completion-default-include-p)
+ (remote-file-name-inhibit-auto-save t)
+ (remote-file-name-inhibit-delete-by-moving-to-trash t)
+ (save-interprogram-paste-before-kill t)
+ (user-full-name "Thomas Voss")
+ (user-mail-address "mail@thomasvoss.com")
+ :config
+ (load custom-file :noerror)
+ (add-hook 'text-mode-hook #'auto-fill-mode)
+ (add-hook 'before-save-hook
+ (defun mm-delete-final-newline ()
+ (let ((end (point-max)))
+ (unless (or require-final-newline
+ mode-require-final-newline
+ (not (= (char-before end) ?\n)))
+ (delete-region (1- end) end)))))
+ (add-hook 'before-save-hook #'delete-trailing-whitespace)
+ (prefer-coding-system 'utf-8))
+
+
+;;; Auto Revert Buffers
+
+(use-package autorevert
+ :custom
+ (global-auto-revert-non-file-buffers t)
+ :init
+ (add-hook
+ 'after-change-major-mode-hook
+ (defun mm-enable-autorevert ()
+ (unless (derived-mode-p 'Buffer-menu-mode)
+ (auto-revert-mode)))))
+
+
+;;; Bookmarks
+
+(use-package bookmark
+ :custom
+ (bookmark-save-flag 0))
+
+
+;;; Automatically Create- and Delete Directories
+
+(defun mm-auto-create-directories (function filename &rest arguments)
+ "Automatically create and delete parent directories of files.
+This is an `:override' advice for `find-file' and friends. It
+automatically creates the parent directories of the file being visited
+if necessary. It also sets a buffer-local variable so that the user
+will be prompted to delete the newly created directories if they kill
+the buffer without saving it."
+ (let (dirs-to-delete)
+ (let* ((dir-to-create (file-name-directory filename))
+ (current-dir dir-to-create))
+ ;; Add each directory component to ‘dirs-to-delete’
+ (while (not (file-exists-p current-dir))
+ (push current-dir dirs-to-delete)
+ (setq current-dir (file-name-directory
+ (directory-file-name current-dir))))
+ (unless (file-exists-p dir-to-create)
+ (make-directory dir-to-create :parents)))
+ (prog1
+ (apply function filename arguments)
+ (when dirs-to-delete
+ (setq-local mm-find-file--dirs-to-delete (reverse dirs-to-delete))
+ (add-hook 'kill-buffer-hook #'mm-find-file--maybe-delete-directories
+ :depth :local)
+ (add-hook 'after-save-hook #'mm-find-file--remove-hooks
+ :depth :local)))))
+
+(defun mm-find-file--maybe-delete-directories ()
+ (unless (file-exists-p buffer-file-name)
+ (dolist (directory mm-find-file--dirs-to-delete)
+ (when (and (stringp directory)
+ (file-exists-p directory)
+ (thread-last
+ (directory-file-name directory)
+ (format "Also delete directory `%s'?")
+ (substitute-quotes)
+ (y-or-n-p)))
+ (delete-directory directory)))))
+
+(defun mm-find-file--remove-hooks ()
+ (remove-hook 'kill-buffer-hook
+ #'mm-find-file--maybe-delete-directories
+ :local)
+ (remove-hook 'after-save-hook
+ #'mm-find-file--remove-hooks
+ :local))
+
+(dolist (command #'(find-file find-alternate-file write-file))
+ (advice-add command :around #'mm-auto-create-directories))
+
+
+;;; Load Modules
+
+(require 'mm-abbrev)
+(require 'mm-buffer-menu)
+(require 'mm-calc)
+(require 'mm-completion)
+(require 'mm-dired)
+(require 'mm-documentation)
+(require 'mm-editing)
+(require 'mm-keybindings)
+;; (require 'mm-modeline)
+(require 'mm-projects)
+(require 'mm-search)
+(require 'mm-tetris)
+(require 'mm-theme)
+(require 'mm-window)
+(when mm-darwin-p
+ (require 'mm-darwin))
+(when mm-humanwave-p
+ (require 'mm-humanwave))
+(when (treesit-available-p)
+ (require 'mm-treesit))
+
+
+;;; Postamble
+
+(add-hook 'after-init-hook
+ (defun mm-echo-init-time ()
+ (message (emacs-init-time "Emacs initialized in %.2f seconds")))
+ 100)
diff --git a/.config/emacs/modules/mm-abbrev.el b/.config/emacs/modules/mm-abbrev.el
new file mode 100644
index 0000000..2154589
--- /dev/null
+++ b/.config/emacs/modules/mm-abbrev.el
@@ -0,0 +1,46 @@
+;;; mm-abbrev.el --- Emacs abbreviations and templates -*- lexical-binding: t; -*-
+
+;;; Helpers
+
+(defmacro mm-abbrev-define-abbreviations (table &rest definitions)
+ "Define abbrevations for an abbreviation TABLE.
+Expand abbrev DEFINITIONS for the given TABLE. DEFINITIONS are a
+sequence of either string pairs mapping an abbreviation to its
+expansion, or a string and symbol pair mapping an abbreviation to a
+function.
+
+After adding all abbreviations to TABLE, this macro marks TABLE as
+case-sensitive to avoid unexpected abbreviation expansions."
+ (declare (indent 1))
+ (unless (cl-evenp (length definitions))
+ (user-error "expected an even number of elements in DEFINITIONS"))
+ `(progn
+ ,@(cl-loop for (abbrev expansion) in (seq-partition definitions 2)
+ if (stringp expansion)
+ collect (list #'define-abbrev table abbrev expansion)
+ else
+ collect (list #'define-abbrev table abbrev "" expansion))
+ (abbrev-table-put ,table :case-fixed t)))
+
+
+;;; Abbreviation Configuration
+
+(use-package abbrev
+ :hook prog-mode
+ :custom
+ (abbrev-file-name (expand-file-name "abbev-defs" mm-data-directory))
+ (save-abbrevs 'silently))
+
+
+;;; Abbreviation Definitions
+
+(use-package python
+ :if mm-humanwave-p
+ :config
+ (mm-abbrev-define-abbreviations python-ts-mode-abbrev-table
+ "empb" "with emphasize.Block():"
+ "empf" "@emphasize.func"
+ "empi" "from shared.system import emphasize"
+ "empt" "emphasize.this"))
+
+(provide 'mm-abbrev)
diff --git a/.config/emacs/modules/mm-buffer-menu.el b/.config/emacs/modules/mm-buffer-menu.el
new file mode 100644
index 0000000..d962447
--- /dev/null
+++ b/.config/emacs/modules/mm-buffer-menu.el
@@ -0,0 +1,15 @@
+;;; mm-buffer-menu.el --- Buffer Menu configuration -*- lexical-binding: t; -*-
+
+(defun mm-buffer-menu-delete-all ()
+ "Mark all buffers for deletion."
+ (interactive nil Buffer-menu-mode)
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (Buffer-menu-delete))))
+
+(use-package buff-menu
+ :bind ( :map Buffer-menu-mode-map
+ ("D" . mm-buffer-menu-delete-all)))
+
+(provide 'mm-buffer-menu)
diff --git a/.config/emacs/modules/mm-calc.el b/.config/emacs/modules/mm-calc.el
new file mode 100644
index 0000000..6c5291a
--- /dev/null
+++ b/.config/emacs/modules/mm-calc.el
@@ -0,0 +1,12 @@
+;;; mm-calc.el --- Emacs configurations for ‘calc-mode’ -*- lexical-binding: t; -*-
+
+(use-package calc
+ :init
+ (setopt
+ calc-display-trail nil
+ calc-group-digits t
+ ;; Optimize for Europeans
+ calc-point-char ","
+ calc-group-char "."))
+
+(provide 'mm-calc)
diff --git a/.config/emacs/modules/mm-completion.el b/.config/emacs/modules/mm-completion.el
new file mode 100644
index 0000000..f84259e
--- /dev/null
+++ b/.config/emacs/modules/mm-completion.el
@@ -0,0 +1,170 @@
+;;; mm-completion.el --- Configuration for Emacs completion -*- lexical-binding: t; -*-
+
+;;; Vertical Completions
+
+(use-package icomplete
+ :hook (after-init . icomplete-vertical-mode)
+ :bind ( :map icomplete-minibuffer-map
+ ("TAB" . #'icomplete-force-complete)
+ ("RET" . #'icomplete-force-complete-and-exit))
+ :config
+ (setq icomplete-scroll t) ; Not ‘defcustom’
+ :custom
+ (icomplete-show-matches-on-no-input t)
+ (icomplete-compute-delay 0))
+
+
+;;; Annotate Completions
+
+;; PKG-EXTERN
+(use-package marginalia
+ :ensure t
+ :hook after-init
+ :custom
+ (marginalia-field-width 50)
+ (marginalia-max-relative-age 0))
+
+
+;;; Minibuffer Completion Styles
+
+(use-package minibuffer
+ :bind ( :map minibuffer-local-completion-map
+ ("SPC" . nil)
+ ("?" . nil))
+ :custom
+ (completion-styles '(basic substring))
+ (completion-category-defaults nil) ; Avoid needing to override things
+ (completion-category-overrides
+ '((file (styles . (basic partial-completion)))
+ (bookmark (styles . (basic substring)))
+ (library (styles . (basic substring)))
+ (imenu (styles . (basic substring)))
+ (consult-location (styles . (basic substring)))
+ (kill-ring (styles . (basic substring)))))
+ (completion-ignore-case t)
+ (read-buffer-completion-ignore-case t)
+ (read-file-name-completion-ignore-case t))
+
+
+;;; Disable Minibuffer Recursion Level
+
+(use-package mb-depth
+ :hook (after-init . minibuffer-depth-indicate-mode)
+ :custom
+ (enable-recursive-minibuffers t))
+
+
+;;; Don’t Show Defaults After Typing
+
+;; Usually if a minibuffer prompt has a default value you can access by
+;; hitting RET, the prompt will remain even if you begin typing (meaning
+;; the default will no longer take effect on RET). Enabling this mode
+;; disables that behaviour.
+
+(use-package minibuf-eldef
+ :hook (after-init . minibuffer-electric-default-mode)
+ :custom
+ (minibuffer-default-prompt-format " [%s]"))
+
+
+;;; Hide Shadowed Filepaths
+
+(use-package rfn-eshadow
+ :hook (after-init . file-name-shadow-mode)
+ :custom
+ (file-name-shadow-properties '(invisible t intangilble t)))
+
+
+;;; Save Minibuffer History
+
+(use-package savehist-mode
+ :hook (after-init . savehist-mode)
+ :custom
+ (history-length 200)
+ (history-delete-duplicates t)
+ :config
+ (add-to-list 'savehist-additional-variables 'kill-ring))
+
+
+;;; Enhanced Replacements for Builtins
+
+;; TODO: Investigate other commands
+;; PKG-EXTERN
+(use-package consult
+ :ensure t
+ :hook (completion-list-mode . consult-preview-at-point-mode)
+ :bind ( ([remap switch-to-buffer] . consult-buffer)
+ ([remap imenu] . consult-imenu)
+ ([remap goto-line] . consult-goto-line)
+ ("M-F" . consult-focus-lines)
+ :map project-prefix-map
+ ("b" . consult-project-buffer)
+ :map consult-narrow-map
+ ("?" . consult-narrow-help))
+ :custom
+ (consult-async-min-input 1)
+ (consult-async-split-style nil)
+ (consult-async-input-debounce .2)
+ (consult-async-input-throttle 0)
+ (consult-find-args
+ (string-join
+ (mapcar #'shell-quote-argument
+ '("find" "." "-not" "("
+ "-path" "*/.git/*" "-prune"
+ "-path" "*/vendor" "-prune"
+ "-path" "*/node_modules" "-prune"
+ ")"))
+ " ")))
+
+
+;;; Dynamic Abbreviations
+
+(use-package dabbrev
+ :commands (dabbrev-completion dabbrev-expand)
+ :custom
+ (dabbrev-upcase-means-case-search t))
+
+
+;;; Finding Things
+
+(use-package find-func
+ :custom
+ (find-library-include-other-files nil))
+
+
+;;; Completion at Point Functions
+
+(defun mm-completions--cape-file-not-dot-path-p (cand)
+ (declare (ftype (function (string) boolean))
+ (pure t) (side-effect-free t))
+ (not (or (string= cand "./")
+ (string= cand "../"))))
+
+;; PKG-EXTERN
+(use-package cape
+ :ensure t
+ :init
+ (add-hook 'completion-at-point-functions
+ (cape-capf-predicate
+ #'cape-file
+ #'mm-completions--cape-file-not-dot-path-p))
+ (add-hook 'completion-at-point-functions
+ (cape-capf-prefix-length #'cape-dabbrev 3)))
+
+
+;;; Completion at Point Live Completions
+
+(use-package completion-preview
+ :hook (after-init . global-completion-preview-mode)
+ :custom
+ (completion-preview-minimum-symbol-length 1))
+
+(use-package completion-preview
+ :after multiple-cursors
+ :config
+ (add-hook 'multiple-cursors-mode-hook
+ (defun mm-completion-set-toggle-previews-on-multiple-cursors ()
+ (global-completion-preview-mode
+ (when multiple-cursors-mode -1)))))
+
+(provide 'mm-completion)
diff --git a/.config/emacs/modules/mm-darwin.el b/.config/emacs/modules/mm-darwin.el
new file mode 100644
index 0000000..6284a8f
--- /dev/null
+++ b/.config/emacs/modules/mm-darwin.el
@@ -0,0 +1,30 @@
+;;; mm-darwin.el --- MacOS Configuration -*- lexical-binding: t; -*-
+
+(unless (featurep 'ns)
+ (error "'NS not available. Something has gone horribly wrong."))
+
+
+;;; Launch Emacs Properly
+
+(defun mm-darwin--ns-raise-emacs ()
+ (ns-do-applescript "tell application \"Emacs\" to activate"))
+
+(add-hook
+ 'after-make-frame-functions
+ (defun mm-darwin--ns-raise-emacs-with-frame (frame)
+ (when (display-graphic-p)
+ (with-selected-frame frame
+ (mm-darwin--ns-raise-emacs)))))
+
+(when (display-graphic-p)
+ (mm-darwin--ns-raise-emacs))
+
+
+;;; Set Modifier Keys
+
+(setopt mac-option-key-is-meta nil
+ mac-command-key-is-meta t)
+(setopt mac-option-modifier 'none
+ mac-command-modifier 'meta)
+
+(provide 'mm-darwin)
diff --git a/.config/emacs/modules/mm-dired.el b/.config/emacs/modules/mm-dired.el
new file mode 100644
index 0000000..d231511
--- /dev/null
+++ b/.config/emacs/modules/mm-dired.el
@@ -0,0 +1,34 @@
+;;; mm-dired.el --- Configure the directory editor -*- lexical-binding: t; -*-
+
+(defun mm-dired-use-current-directory (function &rest args)
+ "Run FUNCTION with ARGS in the current dired directory."
+ (let ((default-directory (dired-current-directory)))
+ (apply function args)))
+
+(use-package dired
+ :hook ((dired-mode . dired-omit-mode)
+ (dired-mode . dired-hide-details-mode))
+ :bind ( :map dired-mode-map
+ ("C-c C-w" . wdired-change-to-wdired-mode)
+ ("f" . dired-x-find-file))
+ :config
+ (advice-add #'dired-x-read-filename-at-point
+ :around #'mm-dired-use-current-directory)
+ :custom
+ (dired-auto-revert-buffer #'dired-directory-changed-p)
+ (dired-dwim-target t)
+ (dired-free-space nil)
+ (dired-recursive-copies 'always)
+ (dired-recursive-deletes 'always)
+ (dired-hide-details-preserved-columns '(1))
+ (dired-listing-switches
+ (combine-and-quote-strings
+ '("-AFGhlv" "--group-directories-first" "--time-style=+%d %b %Y %T"))))
+
+(use-package dired-aux
+ :custom
+ (dired-create-destination-dirs 'ask)
+ (dired-create-destination-dirs-on-trailing-dirsep t)
+ (dired-isearch-filenames 'dwim))
+
+(provide 'mm-dired)
diff --git a/.config/emacs/modules/mm-documentation.el b/.config/emacs/modules/mm-documentation.el
new file mode 100644
index 0000000..51a671f
--- /dev/null
+++ b/.config/emacs/modules/mm-documentation.el
@@ -0,0 +1,46 @@
+;;; mm-documentation.el --- Configuration related to documentation -*- lexical-binding: t; -*-
+
+;;; Enhance Describe Commands
+
+;; PKG-EXTERN
+(use-package helpful
+ :ensure t
+ :bind (([remap describe-command] . helpful-command)
+ ([remap describe-function] . helpful-callable)
+ ([remap describe-key] . helpful-key)
+ ([remap describe-symbol] . helpful-symbol)
+ ([remap describe-variable] . helpful-variable)
+ :map emacs-lisp-mode-map
+ ("C-h C-p" . helpful-at-point)))
+
+
+;;; Open Manpage for Symbol
+
+(defun mm-documentation-man-at-point ()
+ "Open a UNIX manual page for the symbol at point."
+ (interactive nil c-mode c++-mode c-ts-mode c++-ts-mode)
+ (if-let ((symbol
+ (pcase major-mode
+ ((or 'c-mode 'c++-mode)
+ (thing-at-point 'symbol :no-properties))
+ ((or 'c-ts-mode 'c++-ts-mode)
+ (when-let ((node (treesit-thing-at-point "identifier" 'nested)))
+ (treesit-node-text node :no-properties))))))
+ (man symbol)
+ (message "There is no symbol at point.")))
+
+
+;;; Browse RFC Pages
+
+;; PKG-EXTERN
+(use-package rfc-mode
+ :ensure t
+ :custom
+ (rfc-mode-directory (expand-file-name "rfc" (xdg-user-dir "DOCUMENTS")))
+ :config
+ (unless (featurep 'consult)
+ (keymap-set rfc-mode-map "g" #'imenu))
+ (with-eval-after-load 'consult
+ (keymap-set rfc-mode-map "g" #'consult-imenu)))
+
+(provide 'mm-documentation)
diff --git a/.config/emacs/modules/mm-editing.el b/.config/emacs/modules/mm-editing.el
new file mode 100644
index 0000000..5281743
--- /dev/null
+++ b/.config/emacs/modules/mm-editing.el
@@ -0,0 +1,369 @@
+;;; mm-editing.el --- Text editing configuation -*- lexical-binding: t; -*-
+
+;;; Delete Region When Typing
+
+(use-package delsel
+ :hook (after-init . delete-selection-mode))
+
+
+;;; Capitalize ‘ß’ into ‘ẞ’
+
+;; https://lists.gnu.org/archive/html/bug-gnu-emacs/2024-11/msg00030.html
+(set-case-syntax-pair ?ẞ ?ß (standard-case-table))
+(put-char-code-property ?ß 'special-uppercase nil)
+
+
+;;; Force Spaces For Alignment
+
+(defun mm-editing-force-space-indentation (function &rest arguments)
+ "Call FUNCTION with ARGUMENTS in an environment in which
+`indent-tabs-mode' is nil."
+ (let (indent-tabs-mode)
+ (apply function arguments)))
+
+(dolist (command #'(align-region
+ c-backslash-region
+ comment-dwim
+ makefile-backslash-region
+ sh-backslash-region))
+ (advice-add command :around #'mm-editing-force-space-indentation))
+
+
+;;; Indentation Settings
+
+(setq-default
+ tab-width 4
+ indent-tabs-mode (not mm-humanwave-p))
+
+(defvar mm-editing-indentation-settings-alist
+ '((awk-ts-mode . (:extras awk-ts-mode-indent-level))
+ (c-mode . (:extras c-basic-offset))
+ (c-ts-mode . (:extras c-ts-mode-indent-offset))
+ (css-mode . (:extras css-indent-offset))
+ (elixir-ts-mode . (:width 2 :extras elixir-ts-indent-offset))
+ (emacs-lisp-mode . (:width 8 :spaces t)) ; GNU code uses 8-column tabs
+ (go-mod-ts-mode . (:extras go-ts-mode-indent-offset))
+ (go-ts-mode . (:extras go-ts-mode-indent-offset))
+ (gsp-ts-mode . (:width 2 :extras gsp-ts-mode-indent-rules))
+ (helpful-mode . (:width 8)) ; GNU code uses 8-column tabs
+ (json-ts-mode . (:extras json-ts-mode-indent-offset))
+ (latex-mode . (:width 2))
+ (lisp-data-mode . (:spaces t))
+ (lisp-interaction-mode . (:spaces t))
+ (lisp-mode . (:spaces t))
+ (mhtml-mode . (:extras sgml-basic-offset))
+ (org-mode . (:width 8 :spaces t))
+ (python-mode . (:extras python-indent-offset))
+ (python-ts-mode . (:extras python-indent-offset))
+ (sgml-mode . (:extras sgml-basic-offset))
+ (sh-mode . (:extras sh-basic-offset))
+ (sql-mode . (:extras sqlind-basic-offset))
+ (tex-mode . (:width 2))
+ (typescript-ts-mode . (:extras typescript-ts-mode-indent-offset))
+ (vimscript-ts-mode . (:extras vimscript-ts-mode-indent-level))
+ (vue-ts-mode . (:extras (typescript-ts-mode-indent-offset
+ vue-ts-mode-indent-offset))))
+ "Alist of indentation settings.
+Each pair in this alist is of the form (MODE . SETTINGS) where MODE
+specifies the mode for which the given SETTINGS should apply.
+
+SETTINGS is a plist of one-or-more of the following keys:
+
+ `:spaces' -- If nil force tabs for indentation, if non-nil for spaces
+ for indentation. If this key is not provided then the
+ value of `indent-tabs-mode' is used.
+ `:width' -- Specifies a non-negative number to be used as the tab
+ width and indentation offset. If this key is not
+ provided then the default value of `tab-width' is used.
+ `:extras' -- A list of mode-specific variables which control
+ indentation settings that need to be set for
+ configurations to properly be applied.")
+
+(defun mm-editing-set-indentation-settings ()
+ "Set indentation settings for the current major mode.
+The indentation settings are set based on the configured values in
+`mm-editing-indentation-settings-alist'."
+ (let* ((plist (alist-get major-mode mm-editing-indentation-settings-alist))
+ (spaces (plist-member plist :spaces))
+ (width (plist-member plist :width))
+ (extras (plist-member plist :extras)))
+ ;; Some modes like ‘python-mode’ explicitly set ‘tab-width’ and
+ ;; ‘indent-tabs-mode’ so we must override them explicitly.
+ (setq-local indent-tabs-mode (if spaces (not (cadr spaces))
+ (default-value 'indent-tabs-mode))
+ tab-width (or (cadr width) (default-value 'tab-width)))
+ (when extras
+ (setq extras (cadr extras))
+ (when (symbolp extras)
+ (setq extras (list extras)))
+ (dolist (extra extras)
+ (set extra tab-width)))))
+
+(add-hook 'after-change-major-mode-hook #'mm-editing-set-indentation-settings)
+
+(defun mm-editing-set-tabsize ()
+ "Set the tabsize for the current buffer.
+If the current buffer’s major mode requires setting additional variables,
+those should be listed in `mm-editing-indentation-settings-alist'."
+ (declare (interactive-only t))
+ (interactive)
+ (let* ((prompt-default (default-value 'tab-width))
+ (prompt (format-prompt "Tabsize" prompt-default))
+ (tabsize (mm-as-number (read-string prompt nil nil prompt-default))))
+ (setq-local tab-width tabsize)
+ (when-let* ((plist (alist-get major-mode mm-editing-indentation-settings))
+ (extras (plist-get plist :extras)))
+ (dolist (extra (if (symbolp extras)
+ (list extras)
+ extras))
+ (set (make-local-variable extra) tabsize)))))
+
+(use-package sh-mode
+ :custom
+ (sh-indent-for-case-label 0)
+ (sh-indent-for-case-alt #'+))
+
+
+;;; Code Commenting
+
+(defvar mm-editing-comment-settings-alist
+ '(((c-mode c++-mode) . ("/* " " " " */"))
+ ;; rustfmt doesn’t play nice, so we need the ‘*’ comment
+ ;; continuation
+ (rust-mode . ("/* " " * " " */")))
+ "TODO")
+
+(defun mm-newcomment-rust-config ()
+ (setq-local comment-quote-nested nil))
+
+(use-package newcomment
+ :custom
+ (comment-style 'multi-line)
+ :config
+ (dolist (record mm-editing-comment-settings-alist)
+ (let* ((modes (car record))
+ (modes (if (listp modes) modes (list modes)))
+ (config (cdr record))
+ (set-comment-settings
+ (lambda ()
+ (setq-local comment-start (nth 0 config)
+ comment-continue (nth 1 config)
+ comment-end (nth 2 config)))))
+ (dolist (mode modes)
+ (let ((ts-mode (mm-mode-to-ts-mode mode)))
+ (when (fboundp mode)
+ (add-hook (mm-mode-to-hook mode) set-comment-settings))
+ (when (fboundp ts-mode)
+ (add-hook (mm-mode-to-hook ts-mode) set-comment-settings)))))))
+
+
+;;; Multiple Cursors
+
+;; PKG-INTERN
+(use-package multiple-cursors-extensions
+ :after multiple-cursors
+ :bind (("C-M-@" . #'mce-add-cursor-to-next-word)
+ ("C-M-o" . #'mce-add-cursor-to-next-symbol)
+ :map search-map
+ ("$" . #'mce-mark-all-in-region)
+ ("M-$" . #'mce-mark-all-in-region-regexp))
+ :commands (mce-add-cursor-to-next-symbol
+ mce-add-cursor-to-next-word
+ mce-mark-all-in-region
+ mce-mark-all-in-region-regexp
+ mce-sort-regions
+ mce-transpose-cursor-regions))
+
+;; PKG-EXTERN
+(use-package multiple-cursors
+ :ensure t
+ :demand t
+ :bind (("C->" . #'mc/mark-next-like-this)
+ ("C-<" . #'mc/mark-previous-like-this)
+ ("C-M-<" . #'mc/mark-all-like-this-dwim)
+ ("C-M->" . #'mc/edit-lines))
+ :commands ( mm-editing-mark-all-in-region mm-editing-mark-all-in-region-regexp
+ mm-add-cursor-to-next-thing mm-transpose-cursor-regions)
+ :init
+ (with-eval-after-load 'multiple-cursors-core
+ (keymap-unset mc/keymap "<return>" :remove)))
+
+
+;;; Increment Numbers
+
+;; PKG-INTERN
+(use-package increment
+ :bind (("C-c C-a" . #'increment-number-at-point)
+ ("C-c C-x" . #'decrement-number-at-point))
+ :commands (increment-number-at-point decrement-number-at-point))
+
+
+;;; Move Line or Region
+
+(defun mm-editing-move-text-indent (&rest _)
+ (let ((deactivate deactivate-mark))
+ (if (region-active-p)
+ (indent-region (region-beginning) (region-end))
+ (indent-region (line-beginning-position) (line-end-position)))
+ (setq deactivate-mark deactivate)))
+
+;; PKG-EXTERN
+(use-package move-text
+ :ensure t
+ :bind (("M-n" . move-text-down)
+ ("M-p" . move-text-up))
+ :config
+ (dolist (command #'(move-text-up move-text-down))
+ (advice-add command :after #'mm-editing-move-text-indent)))
+
+
+;;; Surround With Delimeters
+
+(defun mm-editing-surround-with-spaces (char)
+ "Surrounds region or current symbol with a pair defined by CHAR.
+This is the same as `surround-insert' except it pads the contents of the
+surround with spaces."
+ (interactive
+ (list (char-to-string (read-char "Character: "))))
+ (let* ((pair (surround--make-pair char))
+ (left (car pair))
+ (right (cdr pair))
+ (bounds (surround--infer-bounds t)))
+ (save-excursion
+ (goto-char (cdr bounds))
+ (insert " " right)
+ (goto-char (car bounds))
+ (insert left " "))
+ (when (eq (car bounds) (point))
+ (forward-char))))
+
+;; TODO: Implement this manually
+;; PKG-EXTERN
+(use-package surround
+ :ensure t
+ :bind-keymap ("M-'" . surround-keymap)
+ :bind (:map surround-keymap
+ ("S" . #'mm-editing-surround-with-spaces))
+ :config
+ (dolist (pair '(("‘" . "’")
+ ("“" . "”")
+ ("»" . "«")
+ ("⟮" . "⟯")))
+ (push pair surround-pairs))
+ (make-variable-buffer-local 'surround-pairs)
+ (add-hook 'emacs-lisp-mode-hook
+ (defun mm-editing-add-elisp-quotes-pair ()
+ (push '("`" . "'") surround-pairs))))
+
+
+;;; Insert Webpage Contents
+
+(defun mm-editing-insert-from-url (url)
+ "Insert the contents of URL at point."
+ (interactive
+ (progn
+ (barf-if-buffer-read-only)
+ (let ((url-at-point (thing-at-point 'url)))
+ (list (read-string
+ (format-prompt "URL" url-at-point)
+ nil nil url-at-point)))))
+ (call-process "curl" nil '(t nil) nil url))
+
+
+;;; Emmet Mode
+
+(defun mm-editing-emmet-dwim (arg)
+ "Do-What-I-Mean Emmet expansion.
+If the region is active then the region will be surrounded by an emmet
+expansion read from the minibuffer. Otherwise the emmet expression
+before point is expanded. When provided a prefix argument the behaviour
+is as described by `emmet-expand-line'."
+ (interactive "*P")
+ (if (region-active-p)
+ (call-interactively #'emmet-wrap-with-markup)
+ (emmet-expand-line arg)))
+
+;; PKG-EXTERN
+(use-package emmet-mode
+ :ensure t
+ :bind ("C-," . mm-editing-emmet-dwim)
+ :custom
+ (emmet-self-closing-tag-style ""))
+
+(defun mm-editing-set-closing-tag-style ()
+ (setq-local emmet-self-closing-tag-style " /"))
+
+(use-package emmet-mode
+ :hook (vue-ts-mode . mm-editing-set-closing-tag-style)
+ :after vue-ts-mode)
+
+
+;;; JQ Manipulation in JSON Mode
+
+;; PKG-INTERN
+(use-package jq
+ :commands (jq-filter-region jq-live))
+
+
+;;; Number Formatting
+
+;; PKG-INTERN
+(use-package number-format-mode
+ :commands ( number-format-buffer number-format-region
+ number-unformat-buffer number-unformat-region
+ number-format-mode))
+
+
+;;; Additional Major Modes
+
+(use-package awk-ts-mode :ensure t) ; PKG-EXTERN
+(use-package cmake-mode :ensure t) ; PKG-EXTERN
+(use-package git-modes :ensure t) ; PKG-EXTERN
+(use-package kdl-mode :ensure t) ; PKG-EXTERN
+(use-package po-mode :ensure t) ; PKG-EXTERN
+(use-package sed-mode :ensure t) ; PKG-EXTERN
+
+;; PKG-EXTERN
+(use-package csv-mode
+ :ensure t
+ :custom
+ (csv-align-style 'auto)
+ (csv-align-padding 2))
+
+(use-package csv-mode
+ :hook (csv-mode . number-format-mode)
+ :after number-format-mode)
+
+;; PKG-INTERN
+(use-package xcompose-mode
+ :vc ( :url "https://git.thomasvoss.com/xcompose-mode"
+ :branch "master"
+ :rev :newest
+ :vc-backend Git)
+ :ensure t)
+
+
+;;; Mode-Specific Configurations
+
+(use-package make-mode
+ :custom
+ (makefile-backslash-column 80))
+
+(use-package python-mode
+ :custom
+ (python-indent-def-block-scale 1)
+ (python-indent-guess-indent-offset-verbose nil))
+
+
+;;; Add Missing Extensions
+
+(dolist (pattern '("\\.tmac\\'" "\\.mom\\'"))
+ (add-to-list 'auto-mode-alist (cons pattern #'nroff-mode)))
+
+
+;;; Subword Navigation
+
+(use-package subword
+ :hook prog-mode)
+
+(provide 'mm-editing)
diff --git a/.config/emacs/modules/mm-humanwave.el b/.config/emacs/modules/mm-humanwave.el
new file mode 100644
index 0000000..9af567e
--- /dev/null
+++ b/.config/emacs/modules/mm-humanwave.el
@@ -0,0 +1,257 @@
+;;; mm-humanwave.el --- Humanwave extras -*- lexical-binding: t; -*-
+
+;;; Query the Backend
+
+(defvar mm-humanwave--query-history nil
+ "History for endpoints given to `mm-humanwave-query'.")
+
+(defun mm-humanwave-query (endpoint &optional method)
+ "Query and display the result of an HTTP request on ENDPOINT.
+If METHOD is nil, a GET request is performed."
+ (interactive
+ (let* ((query (read-string (format-prompt "Query" nil)
+ (car-safe mm-humanwave--query-history)
+ 'mm-humanwave--query-history))
+ (parts (string-split (string-trim query) " " :omit-nulls)))
+ (when (length> parts 2)
+ (user-error "Queries must be of the form `METHOD ENDPOINT' or `ENDPOINT'."))
+ (nreverse parts)))
+ (let* ((project-root (project-root (project-current :maybe-prompt)))
+ (qry-path (expand-file-name "qry" project-root))
+ extras)
+ (unless (file-executable-p qry-path)
+ (user-error "No `qry' executable found in the project root"))
+ (let ((output-buffer (get-buffer-create "*Query Response*")))
+ (with-current-buffer output-buffer
+ (delete-region (point-min) (point-max))
+ (call-process qry-path nil t nil
+ (string-trim endpoint) "-X" (or method "GET"))
+ (unless (eq major-mode 'json-ts-mode)
+ (json-ts-mode))
+ (goto-char (point-min)))
+ (display-buffer output-buffer))))
+
+
+;;; IMenu Support for Handlers
+
+(require 'imenu)
+(require 'which-func)
+
+(defvar mm-humanwave--handler-regexp
+ (rx bol
+ (* blank)
+ (or "if" "elif")
+ (* blank)
+ (or "dialog" "topic" "schedule")
+ (* blank)
+ "=="
+ (* blank)
+ (or ?\' ?\")
+ (group (+ (not (or ?\' ?\"))))
+ (or ?\' ?\")
+ (* blank)
+ ?:
+ (* blank)
+ eol))
+
+(defun mm-humanwave--handler-insert-entry (topic-index function-parts route pos)
+ (if (null function-parts)
+ (cons (cons (format "%s (route)" route) pos) topic-index)
+ (let* ((current-group (car function-parts))
+ (rest-parts (cdr function-parts))
+ (existing-sublist (assoc current-group topic-index)))
+ (if existing-sublist
+ (progn
+ (setcdr existing-sublist
+ (mm-humanwave--handler-insert-entry
+ (cdr existing-sublist) rest-parts route pos))
+ topic-index)
+ (cons (cons current-group
+ (mm-humanwave--handler-insert-entry
+ nil rest-parts route pos))
+ topic-index)))))
+
+(defun mm-humanwave-handler-topic-imenu-index ()
+ (let ((case-fold-search nil)
+ (tree-index (python-imenu-treesit-create-index))
+ (topic-index '()))
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward mm-humanwave--handler-regexp nil :noerror)
+ (let ((route (match-string-no-properties 1))
+ (pos (match-beginning 0))
+ (function-parts (split-string (which-function) "\\.")))
+ (setq topic-index (mm-humanwave--handler-insert-entry
+ topic-index function-parts route pos)))))
+ (append (nreverse topic-index) tree-index)))
+
+(defun mm-humanwave-handler-topic-imenu-setup ()
+ "Setup custom imenu index for `python-ts-mode'."
+ (when (and (string-match-p "/handlers?/" (or (buffer-file-name) ""))
+ (derived-mode-p #'python-ts-mode))
+ (setq-local imenu-create-index-function
+ #'mm-humanwave-handler-topic-imenu-index)))
+
+(add-hook 'after-change-major-mode-hook
+ #'mm-humanwave-handler-topic-imenu-setup)
+
+
+;;; Insert Imports in Vue
+
+(defun mm-humanwave-insert-vue-import-path (base-directory target-file)
+ "Insert an import directive at POINT.
+The import directive imports TARGET-FILE relative from BASE-DIRECTORY.
+When called interactively BASE-DIRECTORY is the directory of the
+current open Vue file and TARGET-FILE is a file in the current project
+that is queried interactively.
+
+When called interactively the prefix argument can be used to emulate
+the behaviour of the INCLUDE-ALL-P argument to
+`mm-humanwave-project-read-file-name'."
+ (interactive
+ (progn
+ (barf-if-buffer-read-only)
+ (list
+ default-directory
+ (mm-humanwave-project-read-file-name current-prefix-arg)))
+ (let ((path (file-name-sans-extension
+ (file-relative-name target-file base-directory))))
+ (unless (string-match-p "/" path)
+ (setq path (concat "./" path)))
+ (insert "import ")
+ (save-excursion
+ (insert (thread-last
+ (file-name-base path)
+ (mm-string-split "-")
+ (mapconcat #'capitalize)))
+ (push-mark (point))
+ (insert (format " from '%s';" path)))))
+
+(defun mm-humanwave-project-read-file-name (&optional include-all-p)
+ "Prompt for a project file.
+This function is similar to `project-find-file', but it returns the
+path to the selected file instead of opening it.
+
+When called interactively the selected file is printed to the
+minibuffer, otherwise it is returned.
+
+The prefix argument INCLUDE-ALL-P is the same as the INCLUDE-ALL
+argument to the `project-find-file' command."
+ (interactive "P")
+ (let* ((project (project-current :maybe-prompt))
+ (root (project-root project))
+ (files (if include-all-p
+ (let ((vc-ignores (mapcar
+ (lambda (dir) (concat dir "/"))
+ vc-directory-exclusion-list)))
+ (project--files-in-directory root vc-ignores))
+ (project-files project)))
+ (canditates (mapcar (lambda (f) (file-relative-name f root))
+ files))
+ (table (lambda (string predicate action)
+ (if (eq action 'metadata)
+ '(metadata (category . file))
+ (complete-with-action action canditates string predicate))))
+ (default-directory root)
+ (choice (completing-read (format-prompt "Find project file" nil)
+ table nil :require-match)))
+ (let ((path (expand-file-name choice root)))
+ (if (called-interactively-p 'any)
+ (message "%s" path)
+ path))))
+
+(defun mm-humanwave-insert-last-commit-message ()
+ "Insert the last commit message at point.
+The inserted commit message will have it’s ticket ID prefix stripped."
+ (interactive "*")
+ (insert
+ (with-temp-buffer
+ (call-process "git" nil t nil "log" "-1" "--pretty=%s")
+ (goto-char (point-min))
+ (replace-regexp "\\`HW-[0-9]+ " "")
+ (string-trim (buffer-string)))))
+
+
+;;; Jira Integration
+
+(use-package jira
+ :ensure t
+ :custom
+ (jira-api-version 3)
+ (jira-base-url "https://humanwave.atlassian.net")
+ (jira-detail-show-announcements nil)
+ (jira-issues-max-results 100)
+ (jira-issues-table-fields '(:key :status-name :assignee-name :summary))
+ (jira-token-is-personal-access-token nil))
+
+
+;;; Icon Autocompletion
+
+(defvar mm-humanwave-icon-component-file "web/src/components/icon.vue"
+ "Path to the <icon /> component definition.")
+
+(defun mm-humanwave--find-icon-map ()
+ (let* ((project (project-current :maybe-prompt))
+ (path (expand-file-name mm-humanwave-icon-component-file
+ (project-root project))))
+ (unless (file-exists-p path)
+ (user-error "File `%s' does not exist." path))
+ (with-current-buffer (finda-file-noselect path)
+ (let* ((parser (treesit-parser-create 'typescript))
+ (root-node (treesit-parser-root-node parser))
+ (query `((((lexical_declaration
+ (variable_declarator
+ name: (identifier) @name)) @the_catch)
+ (:equal @name "ICON_MAP"))
+ (((variable_declaration
+ (variable_declarator
+ name: (identifier) @name)) @the_catch)
+ (:equal @name "ICON_MAP"))))
+ (captures (treesit-query-capture root-node query))
+ (found-node (alist-get 'the_catch captures)))
+ found-node))))
+
+(defun mm-humanwave--icon-list (found-node)
+ (let ((captures (treesit-query-capture found-node '((pair) @the_pair)))
+ (pairs nil))
+ (when captures
+ (dolist (capture captures)
+ (let* ((pair-node (cdr capture))
+ (key-node (treesit-node-child-by-field-name pair-node "key"))
+ (val-node (treesit-node-child-by-field-name pair-node "value")))
+ (when (and key-node val-node)
+ (push (cons (mm-camel-to-lisp
+ (treesit-node-text key-node :no-property))
+ (treesit-node-text val-node :no-property))
+ pairs))))
+ (sort pairs :key #'car :lessp #'string<))))
+
+(defun mm-humanwave-insert-icon-component ()
+ "Insert an icon at point with completion.
+
+This command provides completion for the available props that can be
+given to the <icon /> component. The parser searches for the `ICON_MAP'
+definition in the file specified by `mm-humanwave-icon-component-file'."
+ (interactive "*" vue-ts-mode)
+ (if-let* ((node (mm-humanwave--find-icon-map))
+ (alist (mm-humanwave--icon-list node))
+ (max-key-width
+ (thread-last
+ alist
+ (mapcar (lambda (pair) (length (car pair))))
+ (apply #'max)
+ (+ 4)))
+ (completion-extra-properties
+ `(:annotation-function
+ ,(lambda (key)
+ (concat
+ (propertize " "
+ 'display `(space :align-to ,max-key-width))
+ (propertize (cdr (assoc key alist))
+ 'face 'font-lock-string-face)))))
+ (prompt (format-prompt "Icon" nil))
+ (icon (completing-read prompt alist nil :require-match)))
+ (insert (format "<icon %s />" icon))
+ (error "Unable to find ICON_MAP definitions")))
+
+(provide 'mm-humanwave)
diff --git a/.config/emacs/modules/mm-keybindings.el b/.config/emacs/modules/mm-keybindings.el
new file mode 100644
index 0000000..94782ab
--- /dev/null
+++ b/.config/emacs/modules/mm-keybindings.el
@@ -0,0 +1,185 @@
+;;; mm-keybindings.el --- Emacs keybindings -*- lexical-binding: t; -*-
+
+(require 'editing-functions)
+
+;; The following keys are either unbound and are free to populate, or are
+;; bound to functions I don’t care for:
+;; ‘C-i’, ‘C-j’, ‘C-o’, ‘C-{’, ‘C-}’, ‘C-/’, ‘C-\;’, ‘C-:’
+
+
+;;; Helper Macros
+
+(defmacro mm-keybindings-keymap-set (keymap &rest definitions)
+ "TODO"
+ (declare (indent 1))
+ (unless (cl-evenp (length definitions))
+ (user-error "Expected an even-number of elements in DEFINITIONS."))
+ `(cl-loop for (from to) on (list ,@definitions) by #'cddr
+ do (keymap-set ,keymap from to)))
+
+(defmacro mm-keybindings-keymap-set-repeating (keymap &rest definitions)
+ "TODO"
+ (declare (indent 1))
+ (unless (cl-evenp (length definitions))
+ (user-error "Expected an even-number of elements in DEFINITIONS."))
+ (let ((keymap-gen (gensym "mm-keybindings--repeat-map-")))
+ `(progn
+ (defvar-keymap ,keymap-gen)
+ (cl-loop for (from to) on (list ,@definitions) by #'cddr
+ do (progn
+ (keymap-set ,keymap-gen from to)
+ (put to 'repeat-map ',keymap-gen))))))
+
+(defmacro mm-keybindings-keymap-remap (keymap &rest commands)
+ "Define command remappings for a given KEYMAP.
+COMMANDS is a sequence of unquoted commands. For each pair of
+COMMANDS the first command is remapped to the second command."
+ (declare (indent 1))
+ (unless (cl-evenp (length commands))
+ (user-error "Expected an even-number of elements in COMMANDS."))
+ (macroexp-progn
+ (cl-loop for (from to) in (seq-partition commands 2)
+ collect `(keymap-set
+ ,keymap
+ ,(concat "<remap> <" (symbol-name from) ">")
+ #',to))))
+
+
+;;; Support the Kitty Keyboard Protocol
+
+;; PKG-EXTERN
+(use-package kkp
+ :ensure t
+ :unless (or (display-graphic-p) mm-humanwave-p)
+ :hook (tty-setup . global-kkp-mode))
+
+
+;;; Support QMK Hyper
+
+(defun mm-keybindings-qmk-hyper-as-hyper (args)
+ "Around advice for `keymap-set' to handle QMK hyper."
+ (let ((chord (cadr args)))
+ (when (string-prefix-p "H-" chord)
+ (setf (cadr args) (concat "C-M-S-s" (substring chord 1)))))
+ args)
+
+;; Both ‘keymap-global-set’ and ‘keymap-local-set’ call ‘keymap-set’
+;; internally, so this advice covers all cases
+(advice-add #'keymap-set :filter-args #'mm-keybindings-qmk-hyper-as-hyper)
+
+
+;;; Disable ESC as Meta
+
+(keymap-global-set "<escape>" #'ignore)
+
+
+;;; Enable Repeat Bindings
+
+(defun mm-keybindings-enable-repeat-mode ()
+ "Enable `repeat-mode' without polluting the echo area."
+ (mm-with-suppressed-output
+ (repeat-mode)))
+
+(use-package repeat
+ :hook (after-init . mm-keybindings-enable-repeat-mode)
+ :custom
+ (repeat-exit-timeout 5))
+
+
+;;; Remap Existing Bindings
+
+(mm-keybindings-keymap-remap global-map
+ backward-delete-char-untabify backward-delete-char
+
+ capitalize-word capitalize-dwim
+ downcase-word downcase-dwim
+ upcase-word upcase-dwim
+
+ delete-indentation ef-join-current-and-next-line
+ mark-sexp ef-mark-entire-sexp
+ mark-word ef-mark-entire-word
+ open-line ef-open-line
+ yank ef-yank)
+
+(with-eval-after-load 'cc-vars
+ (setopt c-backspace-function #'backward-delete-char))
+
+
+;;; Remove Unwanted Bindings
+
+(keymap-global-unset "C-x C-c" :remove) ; ‘capitalize-region’
+(keymap-global-unset "C-x C-l" :remove) ; ‘downcase-region’
+(keymap-global-unset "C-x C-u" :remove) ; ‘upcase-region’
+
+;; The following conflicts with ‘ace-window’
+(use-package mhtml-mode
+ :after ace-window
+ :config
+ (keymap-unset html-mode-map "M-o" :remove))
+
+
+;;; Bind Commands Globally
+
+(mm-keybindings-keymap-set global-map
+ "<next>" #'forward-page
+ "<prior>" #'backward-page
+ "C-<next>" #'scroll-up
+ "C-<prior>" #'scroll-down
+
+ "C-." #'repeat
+ "C-^" #'ef-split-line
+ "C-/" #'ef-mark-line-dwim
+ "C-]" #'ef-search-forward-char
+
+ "M-\\" #'cycle-spacing
+
+ "C-c c a" #'mc/vertical-align-with-space
+ "C-c c i" #'mc/insert-numbers
+ "C-c c t" #'mce-transpose-cursor-regions
+ "C-c c s" #'ef-sort-dwim
+ "C-c c f" #'fill-paragraph
+ "C-c d" #'duplicate-dwim)
+
+(mm-keybindings-keymap-set-repeating global-map
+ "j" #'ef-join-current-and-next-line
+ "J" #'join-line)
+
+(mm-keybindings-keymap-set-repeating global-map
+ "n" #'next-error
+ "p" #'previous-error)
+
+(with-eval-after-load 'increment
+ (mm-keybindings-keymap-set-repeating global-map
+ "d" #'decrement-number-at-point
+ "i" #'increment-number-at-point))
+
+
+;;; Other Bindings
+
+(with-eval-after-load 'project
+ (with-eval-after-load 'grab
+ (mm-keybindings-keymap-set project-prefix-map
+ "G" #'project-git-grab))
+
+ (when mm-humanwave-p
+ (mm-keybindings-keymap-set project-prefix-map
+ "q" #'mm-humanwave-query)))
+
+(use-package minibuffer
+ :if mm-humanwave-p
+ :config
+ (mm-keybindings-keymap-set minibuffer-mode-map
+ "C-c m" #'mm-humanwave-insert-last-commit-message))
+
+
+;;; Display Available Keybindings
+
+;; PKG-EXTERN
+(use-package which-key
+ :hook after-init
+ :custom
+ (which-key-dont-use-unicode nil)
+ (which-key-ellipsis "…")
+ (wihch-key-idle-delay .5))
+
+(provide 'mm-keybindings)
diff --git a/.config/emacs/modules/mm-projects.el b/.config/emacs/modules/mm-projects.el
new file mode 100644
index 0000000..6ac35b0
--- /dev/null
+++ b/.config/emacs/modules/mm-projects.el
@@ -0,0 +1,48 @@
+;;; mm-projects.el --- Configuration for project management -*- lexical-binding: t; -*-
+
+;;; Project Configuration
+
+(use-package project
+ :config
+ (unless mm-humanwave-p
+ ;; TODO: Speed this up
+ (if-let ((repo-directory (getenv "REPODIR")))
+ (let* ((list-dir
+ (lambda (path)
+ (directory-files path :full "\\`[^.]")))
+ (directories
+ (cl-loop for author in (funcall list-dir (getenv "REPODIR"))
+ append (cl-loop for path in (funcall list-dir author)
+ collect (list (concat path "/"))))))
+ (with-temp-buffer
+ (prin1 directories (current-buffer))
+ (write-file project-list-file))
+ (project--read-project-list))
+ (warn "The REPODIR environment variable is not set."))))
+
+
+;;; Version Control Support
+
+(use-package vc-hooks
+ :custom
+ (vc-follow-symlinks t)
+ (vc-handled-backends '(Git)))
+
+
+;; Project Compilation
+
+(use-package compile
+ :config
+ (require 'ansi-color)
+ (add-hook 'compilation-filter-hook #'ansi-color-compilation-filter))
+
+
+;;; GitHub Pull Requests
+
+;; PKG-INTERN
+(use-package gh
+ :bind (("C-c p c" . #'gh-create-pr)
+ ("C-c p o" . #'gh-open-previous-pr))
+ :commands (gh-create-pr gh-open-previous-pr))
+
+(provide 'mm-projects)
diff --git a/.config/emacs/modules/mm-search.el b/.config/emacs/modules/mm-search.el
new file mode 100644
index 0000000..7c9fece
--- /dev/null
+++ b/.config/emacs/modules/mm-search.el
@@ -0,0 +1,26 @@
+;;; mm-search.el --- Emacs text searching -*- lexical-binding: t; -*-
+
+;;; Classic Emacs text search
+
+(use-package isearch
+ :demand t
+ :custom
+ (search-whitespace-regexp ".*?")
+ (isearch-lax-whitespace t)
+ (isearch-regexp-lax-whitespace nil)
+ (isearch-lazy-count t)
+ (lazy-highlight-initial-delay 0)
+ (lazy-count-prefix-format "%d/%d ")
+ (isearch-repeat-on-direction-change t))
+
+
+;;; Grab Integration
+
+;; PKG-INTERN
+(use-package grab
+ :commands ( grab git-grab project-grab project-git-grab
+ dired-grab-marked-files)
+ :custom
+ (grab-default-pattern '("x/^.*?$/ g// h//" . 12)))
+
+(provide 'mm-search)
diff --git a/.config/emacs/modules/mm-tetris.el b/.config/emacs/modules/mm-tetris.el
new file mode 100644
index 0000000..2a0a206
--- /dev/null
+++ b/.config/emacs/modules/mm-tetris.el
@@ -0,0 +1,19 @@
+;;; mm-tetris.el --- Emacs configurations for ‘tetris’ -*- lexical-binding: t; -*-
+
+(defun mm-tetris-rotate-mirror ()
+ "Rotate the current piece by 180°."
+ (interactive nil tetris-mode)
+ (tetris-rotate-next)
+ (tetris-rotate-next))
+
+(use-package tetris
+ :bind ( :map tetris-mode-map
+ ("a" . tetris-move-left)
+ ("d" . tetris-move-right)
+ ("k" . tetris-rotate-next)
+ (";" . tetris-rotate-prev)
+ ("l" . tetris-move-down)
+ ("o" . mm-tetris-rotate-mirror)
+ ("SPC" . tetris-move-bottom)))
+
+(provide 'mm-tetris)
diff --git a/.config/emacs/modules/mm-theme.el b/.config/emacs/modules/mm-theme.el
new file mode 100644
index 0000000..572064b
--- /dev/null
+++ b/.config/emacs/modules/mm-theme.el
@@ -0,0 +1,232 @@
+;;; mm-theme.el --- Emacs theme settings -*- lexical-binding: t; -*-
+
+
+;;; Themes
+
+(setopt custom-theme-directory (expand-file-name "themes" mm-config-directory))
+(load-theme 'mango-dark :no-confirm)
+
+
+;;; Disable Cursor Blink
+
+(use-package frame
+ :config
+ (blink-cursor-mode -1))
+
+
+;;; Fonts
+
+(defvar mm-theme-monospace-font `(,(if mm-humanwave-p
+ "Iosevka Custom"
+ "Iosevka Smooth")
+ :weight regular
+ :height 162)
+ "The default monospace font.
+This is a plist containing a font name, -weight, and -height.")
+
+(defvar mm-theme-proportional-font
+ ;; TODO: SF font?
+ `(,(if mm-darwin-p "Microsoft Sans Serif" "SF Pro Text")
+ :weight regular :height 162)
+ "The default proportional font.
+This is a plist containing a font name, -weight, and -height.")
+
+(defun mm-theme-set-fonts (&optional _frame)
+ "Set frame font settings.
+Sets the frame font settings according to the fonts specified by
+`mm-theme-monospace-font' and `mm-theme-proportional-font'.
+
+This function can be used as a hook in `after-make-frame-functions' and
+_FRAME is ignored."
+ (interactive)
+ (let* ((mono-family (car mm-theme-monospace-font))
+ (mono-props (cdr mm-theme-monospace-font))
+ (prop-family (car mm-theme-proportional-font))
+ (prop-props (cdr mm-theme-proportional-font))
+ (mono-weight (plist-get mono-props :weight))
+ (mono-height (plist-get mono-props :height))
+ (prop-weight (plist-get prop-props :weight))
+ (prop-height (plist-get prop-props :height)))
+ ;; Some characters in this font are larger than usual
+ (when (string= mono-family "Iosevka Smooth")
+ (dolist (rune '(?․ ?‥ ?… ?— ?← ?→ ?⇐ ?⇒ ?⇔))
+ (set-char-table-range char-width-table rune 2)))
+ (set-face-attribute 'default nil
+ :font mono-family
+ :weight mono-weight
+ :height mono-height)
+ (set-face-attribute 'fixed-pitch nil
+ :font mono-family
+ :weight mono-weight
+ :height mono-height)
+ (set-face-attribute 'variable-pitch nil
+ :font prop-family
+ :weight prop-weight
+ :height prop-height)))
+
+(if (daemonp)
+ (add-hook 'after-make-frame-functions #'mm-theme-set-fonts)
+ (mm-theme-set-fonts))
+
+
+;;; Ligature Support
+
+(defvar mm-theme-ligatures-alist
+ `(((c-mode c++-mode)
+ . ("->"))
+ ((c++-mode)
+ . ("::"))
+ ((js-mode typescript-ts-mode vue-ts-mode)
+ . (("=" ,(rx (or ?> (** 1 2 ?=))))
+ ("!" ,(rx (** 1 2 ?=)))))
+ (go-ts-mode
+ . (":=" "<-"))
+ ((python-mode)
+ . (":=" "->"))
+ ((mhtml-mode html-mode vue-ts-mode)
+ . ("<!--" "-->" "/>"))
+ (prog-mode
+ . ("<<=" "<=" ">=" "==" "!=" "*=" ("_" "_+"))))
+ "Ligatures to enable in specific modes.
+Elements of this alist are of the form:
+
+ (SPEC . LIGATURES)
+
+Where LIGATURES is a list of ligatures to enable for the set of modes
+described by SPEC.
+
+SPEC can be either a symbol, or a list of symbols. These symbols
+should correspond to modes for which the associated LIGATURES should
+be enabled.
+
+A mode may also be specified in multiple entries. To configure
+`go-ts-mode' to have its set of ligatures be a super-set of the
+ligatures for `c-ts-mode', the following two entries could be added:
+
+ \\='((c-ts-mode go-ts-mode) . (\">=\" \"<=\" \"!=\" \"==\"))
+ \\='(go-ts-mode . (\":=\"))
+
+When a language is specified and it’s tree-sitter compatriot is bound,
+then LIGATURES are bound for both modes.")
+
+(defun mm-theme-update-ligatures ()
+ "Update the ligature composition tables.
+After running this function you may need to restart `ligature-mode'.
+
+Also see `mm-theme-ligatures-alist'."
+ (interactive)
+ (setopt ligature-composition-table nil)
+ (cl-loop for (spec . ligatures) in mm-theme-ligatures-alist
+ do (ligature-set-ligatures spec ligatures)
+ (cl-loop for mode in spec
+ when (fboundp (mm-mode-to-ts-mode mode))
+ do (ligature-set-ligatures mode ligatures))))
+
+;; PKG-EXTERN
+(use-package ligature
+ :ensure t
+ :if (and (display-graphic-p)
+ (or (seq-contains-p (split-string system-configuration-features)
+ "HARFBUZZ")
+ mm-darwin-p))
+ :hook prog-mode
+ :config
+ (mm-theme-update-ligatures))
+
+
+;;; Background Opacity
+
+(defvar mm-theme-background-opacity 100
+ "Opacity of the graphical Emacs frame.
+A value of 0 is fully transparent while 100 is fully opaque.")
+
+(defun mm-theme-set-background-opacity (opacity)
+ "Set the current frames' background opacity.
+See also the `mm-theme-background-opacity' variable."
+ (interactive
+ (list (mm-as-number
+ (read-string
+ (format-prompt "Background opacity"
+ (default-value 'mm-theme-background-opacity))
+ nil nil mm-theme-background-opacity))))
+ (set-frame-parameter nil 'alpha-background opacity))
+
+(add-to-list
+ 'default-frame-alist (cons 'alpha-background mm-theme-background-opacity))
+
+
+;;; Divider Between Windows
+
+(use-package frame
+ :hook (after-init . window-divider-mode))
+
+
+;;; In-Buffer Highlighting
+
+;; PKG-INTERN
+(use-package highlighter
+ :bind (("C-c h m" . #'highlighter-mark)
+ ("C-c h u" . #'highlighter-unmark)
+ ("C-c h U" . #'highlighter-unmark-buffer))
+ :commands (highlighter-mark highlighter-unmark highlighter-unmark-buffer)
+ :init
+ (require 'hi-lock)) ; For extra face definitions
+
+
+;;; Pretty Page Boundaries
+
+;; TODO: Implement this myself?
+(use-package page-break-lines
+ :ensure t
+ :hook (after-init . global-page-break-lines-mode)
+ :config
+ (dolist (mode '(c-mode c++-mode gsp-ts-mode))
+ (add-to-list 'page-break-lines-modes mode)
+ (let ((ts-mode (mm-mode-to-ts-mode mode)))
+ (when (fboundp ts-mode)
+ (add-to-list 'page-break-lines-modes ts-mode))))
+ (add-hook
+ 'change-major-mode-hook
+ (defun mm-theme--set-page-break-max-width ()
+ (setopt page-break-lines-max-width fill-column)))
+ ;; Since the ‘^L’ character is replaced by a horizontal rule, the
+ ;; cursor should appear below the horizontal rule. When moving
+ ;; backwards we need to account for the fact that the cursor is
+ ;; actually one character ahead of hte page break and adjust
+ ;; accordingly.
+ (advice-add
+ #'forward-page :after
+ (defun mm-theme--forward-char (&rest _)
+ (forward-char)))
+ (advice-add
+ #'backward-page :before
+ (defun mm-theme--backward-char (&rest _)
+ (backward-char))))
+
+
+;;; Line Highlighting
+
+(use-package hl-line
+ :custom
+ (hl-line-sticky-flag nil))
+
+
+;;; Indent Guides
+
+(when mm-humanwave-p
+ (use-package highlight-indent-guides
+ :ensure t
+ :hook ((jinja2-mode vue-ts-mode mhtml-mode) . highlight-indent-guides-mode)
+ :custom
+ (highlight-indent-guides-method 'fill)
+ (highlight-indent-guides-auto-even-face-perc 30)
+ (highlight-indent-guides-auto-odd-face-perc 0)))
+
+
+;;; Instantly highlight matching parens
+
+(use-package paren
+ :custom
+ (show-paren-delay 0))
+
+(provide 'mm-theme)
diff --git a/.config/emacs/modules/mm-treesit.el b/.config/emacs/modules/mm-treesit.el
new file mode 100644
index 0000000..9c983a0
--- /dev/null
+++ b/.config/emacs/modules/mm-treesit.el
@@ -0,0 +1,261 @@
+;;; mm-treesit.el --- Tree-Sitter configuration -*- lexical-binding: t; -*-
+
+(require 'treesit)
+
+;;; Tree-Sitter Variables
+
+(defvar mm-treesit-language-remap-alist
+ '((cpp . c++)
+ (gomod . go-mod)
+ (javascript . js)
+ (vim . vimscript))
+ "TODO")
+
+(defun mm-treesit--map-language (language)
+ (declare (ftype (function (symbol) symbol))
+ (pure t) (side-effect-free t))
+ (alist-get language mm-treesit-language-remap-alist language))
+
+(defun mm-treesit--language-exists-p (language)
+ (declare (ftype (function (symbol) boolean))
+ (pure t) (side-effect-free t))
+ (thread-last
+ (mm-treesit--map-language language)
+ (format "%s-ts-mode")
+ (intern)
+ (fboundp)))
+
+(setopt treesit-font-lock-level 2)
+(setopt treesit-language-source-alist
+ '((awk
+ "https://github.com/Beaglefoot/tree-sitter-awk")
+ (c
+ "https://github.com/tree-sitter/tree-sitter-c")
+ (cpp
+ "https://github.com/tree-sitter/tree-sitter-cpp")
+ (css
+ "https://github.com/tree-sitter/tree-sitter-css")
+ (dockerfile
+ "https://github.com/camdencheek/tree-sitter-dockerfile")
+ (elixir
+ "https://github.com/elixir-lang/tree-sitter-elixir")
+ (go
+ "https://github.com/tree-sitter/tree-sitter-go")
+ (gomod
+ "https://github.com/camdencheek/tree-sitter-go-mod")
+ (gsp
+ "git://git.thomasvoss.com/tree-sitter-gsp.git")
+ (heex
+ "https://github.com/phoenixframework/tree-sitter-heex")
+ (html
+ "https://github.com/tree-sitter/tree-sitter-html")
+ (java
+ "https://github.com/tree-sitter/tree-sitter-java")
+ (javascript
+ "https://github.com/tree-sitter/tree-sitter-javascript")
+ (json
+ "https://github.com/tree-sitter/tree-sitter-json")
+ (markdown
+ "https://github.com/tree-sitter-grammars/tree-sitter-markdown"
+ "split_parser" "tree-sitter-markdown/src")
+ (markdown-inline
+ "https://github.com/tree-sitter-grammars/tree-sitter-markdown"
+ "split_parser" "tree-sitter-markdown-inline/src")
+ (python
+ "https://github.com/tree-sitter/tree-sitter-python")
+ (rust
+ "https://github.com/tree-sitter/tree-sitter-rust")
+ (tsx
+ "https://github.com/tree-sitter/tree-sitter-typescript"
+ "master" "tsx/src")
+ (typescript
+ "https://github.com/tree-sitter/tree-sitter-typescript"
+ "master" "typescript/src")
+ (vim
+ "https://github.com/tree-sitter-grammars/tree-sitter-vim")
+ (vue
+ "https://github.com/ikatyang/tree-sitter-vue")
+ (yaml
+ "https://github.com/tree-sitter-grammars/tree-sitter-yaml")))
+
+
+;;; Install Missing Parsers
+
+(defun mm-treesit-install-all ()
+ "Install all Tree-Sitter parsers.
+This is like `mm-treesit-install-missing' but also reinstalls parsers
+that are already installed."
+ (interactive)
+ (cl-loop for (lang) in treesit-language-source-alist
+ when (mm-treesit--language-exists-p lang)
+ do (treesit-install-language-grammar lang)))
+
+(defun mm-treesit-install-missing ()
+ "Install missing Tree-Sitter parsers.
+The parsers are taken from `treesit-language-source-alist'."
+ (interactive)
+ (cl-loop for (lang) in treesit-language-source-alist
+ unless (or (treesit-language-available-p lang)
+ (not (mm-treesit--language-exists-p lang)))
+ do (treesit-install-language-grammar lang)))
+
+(mm-treesit-install-missing)
+
+
+;;; Install Additional TS Modes
+
+;; PKG-INTERN
+(use-package gsp-ts-mode
+ :vc (:url "https://git.thomasvoss.com/gsp-ts-mode"
+ :branch "master"
+ :rev :newest
+ :vc-backend Git)
+ :ensure t)
+
+;; NOTE: This package doesn’t autoload its ‘auto-mode-alist’ entries
+;; PKG-EXTERN
+(use-package vimscript-ts-mode
+ :ensure t
+ :mode (rx (or (seq (? (or ?. ?_)) (? ?g) "vimrc")
+ ".vim"
+ ".exrc")
+ eos))
+
+;; NOTE: This package doesn’t autoload its ‘auto-mode-alist’ entries
+;; PKG-EXTERN
+(use-package vue-ts-mode
+ :vc ( :url "https://github.com/8uff3r/vue-ts-mode.git"
+ :branch "main"
+ :rev :newest
+ :vc-backend Git)
+ :ensure t
+ :mode "\\.vue\\'")
+
+;; NOTE: This package doesn’t autoload its ‘auto-mode-alist’ entries
+;; PKG-EXTERN
+(use-package markdown-ts-mode
+ :ensure t
+ :mode "\\.md\\'")
+
+
+;;; Prefer Tree-Sitter Modes
+
+;; NOTE: ‘go-ts-mode’ already adds itself to ‘auto-mode-alist’ but it
+;; isn’t autoloaded as of 2024-09-29 so we need to do it ourselves
+;; anyway. Same goes for ‘typescript-ts-mode’.
+(defvar mm-treesit-language-file-name-alist
+ '((dockerfile . "/[Dd]ockerfile\\'")
+ (elixir . "\\.exs?\\'")
+ (go . "\\.go\\'")
+ (gomod . "/go\\.mod\\'")
+ (heex . "\\.heex\\'")
+ (json . "\\.json\\'")
+ (rust . "\\.rs\\'")
+ (tsx . "\\.tsx\\'")
+ (typescript . "\\.ts\\'")
+ (yaml . "\\.ya?ml\\'"))
+ "Alist mapping languages to their associated file-names.
+This alist is a set of pairs of the form (LANG . REGEXP) where LANG is
+the symbol corresponding to a major mode with the `-ts-mode' suffix
+removed. REGEXP is a regular expression matching filenames for which
+the associated language’s major-mode should be enabled.
+
+This alist is used to configure `auto-mode-alist'.")
+
+(defvar mm-treesit-dont-have-modes
+ '(markdown-inline)
+ "List of languages that don't have modes.
+Some languages may come with multiple parsers, (e.g. `markdown' and
+`markdown-inline') and as a result one-or-more of the parsers won't be
+associated with a mode. To avoid breaking the configuration, these
+languages should be listed here.")
+
+(dolist (spec treesit-language-source-alist)
+ (let* ((lang (car spec))
+ (lang-remap (mm-treesit--map-language lang))
+ (name-mode (intern (format "%s-mode" lang-remap)))
+ (name-ts-mode (intern (format "%s-ts-mode" lang-remap))))
+ ;; If ‘name-ts-mode’ is already in ‘auto-mode-alist’ then we don’t
+ ;; need to do anything, however if that’s not the case then if
+ ;; ‘name-ts-mode’ and ‘name-mode’ are both bound we do a simple
+ ;; remap. If the above is not true then we lookup the extensions in
+ ;; ‘mm-treesit-language-file-name-alist’.
+ (cond
+ ((memq lang mm-treesit-dont-have-modes)
+ nil)
+ ((not (fboundp name-ts-mode))
+ nil)
+ ((rassq name-ts-mode auto-mode-alist)
+ nil)
+ ((fboundp name-mode)
+ (add-to-list 'major-mode-remap-alist (cons name-mode name-ts-mode)))
+ (:else
+ (if-let ((file-regexp
+ (alist-get lang mm-treesit-language-file-name-alist)))
+ (add-to-list 'auto-mode-alist (cons file-regexp name-ts-mode))
+ (warn "Unable to determine the extension for `%s'." name-ts-mode))))))
+
+;; JavaScript being difficult as usual
+(add-to-list 'major-mode-remap-alist '(javascript-mode . js-ts-mode))
+
+
+;;; Hack For C23
+
+(advice-add #'c-ts-mode--keywords :filter-return
+ (defun mm-c-ts-mode-add-constexpr (keywords)
+ ;; NOTE: We can’t add ‘typeof’ until it’s added to the TS grammar
+ ;; https://github.com/tree-sitter/tree-sitter-c/issues/236
+ (append keywords '("constexpr"))))
+
+
+;;; Highlight Predefined Variables
+
+(defun mm-treesit-c-apply-font-lock-extras ()
+ (setq treesit-font-lock-settings
+ (append treesit-font-lock-settings
+ mm-treesit--c-font-lock-rules)))
+
+(use-package c-ts-mode
+ :hook (c-ts-mode . mm-treesit-c-apply-font-lock-extras))
+
+(defvar mm-treesit--c-font-lock-rules
+ (treesit-font-lock-rules
+ :language 'c
+ :feature 'constant
+ :override t
+ `(((identifier) @font-lock-constant-face
+ (:match ,(rx bos (or "__func__" "__FUNCTION__") eos)
+ @font-lock-constant-face)))))
+
+
+;;; Region Expansion
+
+(defun mm-treesit-expreg-expand (n)
+ "Expand to N syntactic units."
+ (interactive "p")
+ (dotimes (_ n)
+ (expreg-expand)))
+
+(defun mm-treesit-expreg-expand-dwim ()
+ "Do-What-I-Mean `expreg-expand' to start with symbol or word.
+If over a real symbol, mark that directly, else start with a word. Fall
+back to regular `expreg-expand'."
+ (interactive)
+ (if (region-active-p)
+ (expreg-expand)
+ (let ((symbol (bounds-of-thing-at-point 'symbol)))
+ (cond
+ ((equal (bounds-of-thing-at-point 'word) symbol)
+ (mm-treesit-expreg-expand 1))
+ (symbol
+ (mm-treesit-expreg-expand 2))
+ (:else
+ (expreg-expand))))))
+
+;; PKG-EXTERN
+(use-package expreg
+ :ensure t
+ :commands (mm-treesit-expreg-expand mm-treesit-expreg-expand-dwim)
+ :bind ("M-SPC" . mm-treesit-expreg-expand-dwim))
+
+(provide 'mm-treesit)
diff --git a/.config/emacs/modules/mm-window.el b/.config/emacs/modules/mm-window.el
new file mode 100644
index 0000000..dcbf5b6
--- /dev/null
+++ b/.config/emacs/modules/mm-window.el
@@ -0,0 +1,79 @@
+;;; mm-window.el --- Window configurations -*- lexical-binding: t; -*-
+
+;;; Unique Buffer Names
+
+(use-package uniquify
+ :custom
+ (uniquify-buffer-name-style 'forward))
+
+
+;;; Highlight Whitespace
+
+(use-package whitespace
+ :bind (("<f1>" . whitespace-mode)
+ ("C-c z" . delete-trailing-whitespace))
+ :custom
+ (whitespace-style
+ '( face trailing spaces tabs space-mark tab-mark empty indentation
+ space-after-tab space-before-tab))
+ (whitespace-display-mappings
+ '((space-mark 32 [?·] [?.]) ; Space
+ (space-mark 160 [?␣] [?_]) ; Non-Breaking Space
+ (tab-mark 9 [?» ?\t] [?> ?\t]))))
+
+
+;;; Line Numbers
+
+(use-package display-line-numbers
+ :bind ("<f2>" . display-line-numbers-mode)
+ :custom
+ (display-line-numbers-grow-only t)
+ (display-line-numbers-type 'relative)
+ (display-line-numbers-width-start 99))
+
+
+;;; Select Help Windows
+
+(use-package help
+ :custom
+ (help-window-select t))
+
+
+;;; Window Scrolling
+
+(use-package window
+ :custom
+ (scroll-conservatively 101) ; (info "(Emacs)Auto Scrolling")
+ (scroll-error-top-bottom t)
+ (scroll-margin 10)
+ :config
+ (setq-default truncate-partial-width-windows nil))
+
+
+;;; Smoother Scrolling
+
+(mm-comment
+ (use-package pixel-scroll
+ :init
+ (pixel-scroll-precision-mode)
+ :config
+ ;; Make it easier to use custom scroll functions
+ (dolist (binding '("<next>" "<prior>"))
+ (keymap-unset pixel-scroll-precision-mode-map binding :remove))))
+
+
+;;; Ace Window
+
+;; PKG-EXTERN
+(use-package ace-window
+ :ensure t
+ :bind ("M-o" . ace-window)
+ :custom
+ (aw-make-frame-char ?.)
+ (aw-scope 'frame)
+ ;; Use uppercase labels because they look nicer, but allow selecting
+ ;; with lowercase so that I don’t need to hold shift.
+ (aw-keys (cl-loop for x from ?A to ?Z collect x))
+ (aw-translate-char-function #'upcase))
+
+(provide 'mm-window)
diff --git a/.config/emacs/site-lisp/editing-functions.el b/.config/emacs/site-lisp/editing-functions.el
new file mode 100644
index 0000000..bcd1b47
--- /dev/null
+++ b/.config/emacs/site-lisp/editing-functions.el
@@ -0,0 +1,163 @@
+;;; editing-functions.el --- Text editing commands -*- lexical-binding: t; -*-
+
+(defun ef-join-current-and-next-line (&optional arg beg end)
+ "Join the current- and next lines.
+This function is identical to `join-line' but it joins the current
+line with the next one instead of the previous one."
+ (interactive
+ (progn (barf-if-buffer-read-only)
+ (cons current-prefix-arg
+ (and (use-region-p)
+ (list (region-beginning) (region-end))))))
+ (delete-indentation
+ (unless (or beg end) (not arg))
+ beg end))
+
+(defun ef-mark-entire-word (&optional arg allow-extend)
+ "Mark ARG words beginning at point.
+This command is a wrapper around `mark-word' that moves the point such
+that the word under point is entirely marked. ARG and ALLOW-EXTEND
+are just as they are with `mark-word.'"
+ (interactive "P\np")
+ (if (eq last-command this-command)
+ (mark-word arg allow-extend)
+ (let ((bounds (bounds-of-thing-at-point 'word))
+ (numeric-arg (or allow-extend 0)))
+ (if bounds
+ (goto-char (if (< numeric-arg 0)
+ (cdr bounds)
+ (car bounds)))
+ (forward-to-word (when (< numeric-arg 0) -1))))
+ (mark-word arg allow-extend)))
+
+(defun ef-mark-entire-sexp (&optional arg allow-extend)
+ "Mark ARG sexps beginning at point.
+This command is a wrapper around `mark-sexp' that moves the point such
+that the sexp under point is entirely marked. ARG and ALLOW-EXTEND
+are just as they are with `mark-sexp.'"
+ (interactive "P\np")
+ (if (eq last-command this-command)
+ (mark-sexp arg allow-extend)
+ (let ((bounds (bounds-of-thing-at-point 'sexp))
+ (numeric-arg allow-extend))
+ (if bounds
+ (goto-char (if (< numeric-arg 0)
+ (cdr bounds)
+ (car bounds)))
+ (if (< numeric-arg 0)
+ (progn
+ (backward-sexp)
+ (forward-sexp))
+ (forward-sexp)
+ (backward-sexp))))
+ (mark-sexp arg allow-extend)))
+
+(defun ef-mark-line-dwim (&optional arg)
+ "Mark ARG lines beginning at point.
+If the region is active then it is extended by ARG lines. If called
+without a prefix argument this command marks one line forwards unless
+point is ahead of the mark in which case this command marks one line
+backwards.
+
+If this function is called with a negative prefix argument and no
+region active, the current line is marked."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((numeric-arg (prefix-numeric-value arg)))
+ (if (region-active-p)
+ (progn
+ (exchange-point-and-mark)
+ (goto-char (1+ (pos-eol (if arg numeric-arg
+ (when (< (point) (mark)) -1)))))
+ (exchange-point-and-mark))
+ (if (< numeric-arg 0)
+ (progn
+ (push-mark (pos-bol (+ 2 numeric-arg)) nil :activate)
+ (goto-char (1+ (pos-eol))))
+ (push-mark (1+ (pos-eol numeric-arg)) nil :activate)
+ (goto-char (pos-bol))))))
+
+(defun ef-open-line (arg)
+ "Insert and move to a new empty line after point.
+With prefix argument ARG, inserts and moves to a new empty line before
+point."
+ (interactive "*P")
+ (end-of-line)
+ (newline-and-indent)
+ (when arg
+ (transpose-lines 1)
+ (previous-line 2)
+ (end-of-line)))
+
+(defun ef-split-line (&optional above)
+ "Split the line at point.
+Place the contents after point on a new line, indenting the new line
+according to the current major mode. With prefix argument ABOVE the
+contents after point are placed on a new line before point."
+ (interactive "*P")
+ (save-excursion
+ (let* ((start (point))
+ (end (pos-eol))
+ (string (buffer-substring start end)))
+ (delete-region start end)
+ (when above
+ (goto-char (1- (pos-bol))))
+ (newline)
+ (insert string)
+ (indent-according-to-mode))))
+
+(defun ef-sort-dwim (&optional reversep)
+ "Sort regions do-what-i-mean.
+When multiple cursors are not being used this command functions just
+like `sort-lines' with the start- and end bounds set to the current
+region beginning and -end.
+
+When using multiple cursors this command sorts the regions marked by
+each cursor (effectively calling `emc-sort-regions'.
+
+When called with a prefix argument REVERSEP, sorting occurs in reverse
+order."
+ (interactive "*P")
+ (if (and (featurep 'multiple-cursors-extensions)
+ (< 1 (mc/num-cursors)))
+ (emc-sort-regions reversep)
+ (sort-lines reversep (region-beginning) (region-end))))
+
+(defun ef-yank (&optional arg)
+ "Yank text from the kill-ring.
+Yank the most recent kill from the kill ring via `yank'. If called
+with prefix argument ARG then interactively yank from the kill ring
+via `yank-from-kill-ring'.
+
+If `consult' is available than this command instead calls
+`consult-yank-from-kill-ring' when called with non-nil ARG."
+ (declare (interactive-only t))
+ (interactive "*P")
+ ;; Avoid ‘current-prefix-arg’ cascading down to ‘yank-from-kill-ring’
+ (let (current-prefix-arg)
+ (cond ((null arg)
+ (yank))
+ ((featurep 'consult)
+ (call-interactively #'consult-yank-from-kill-ring))
+ (:else
+ (call-interactively #'yank-from-kill-ring)))))
+
+(defun ef-search-forward-char (char &optional n)
+ "Search forwards to the Nth occurance of CHAR.
+If called interactively CHAR is read from the minibuffer and N is
+given by the prefix argument.
+
+If N is negative then this function searches backwards.
+
+When searching forwards point is left before CHAR while when searching
+backwards point is left after CHAR."
+ (interactive
+ (list (read-char)
+ (prefix-numeric-value current-prefix-arg)))
+ (when (and (> n 0) (= char (char-after (point))))
+ (forward-char))
+ (search-forward (char-to-string char) nil nil n)
+ (when (> n 0)
+ (backward-char)))
+
+(provide 'editing-functions)
diff --git a/.config/emacs/site-lisp/gh.el b/.config/emacs/site-lisp/gh.el
new file mode 100644
index 0000000..19afaec
--- /dev/null
+++ b/.config/emacs/site-lisp/gh.el
@@ -0,0 +1,68 @@
+;;; gh.el --- GitHub integration for Emacs -*- lexical-binding: t; -*-
+
+(defvar gh-pr-regexp
+ "\\`https://\\(?:www\\.\\)?github\\.com/[^/]+/[^/]+/pull/[[:digit:]]+\\'"
+ "TODO")
+
+(defsubst gh--pr-link-p (s)
+ (declare (ftype (function (string) boolean))
+ (pure t) (side-effect-free t))
+ (string-match-p gh-pr-regexp s))
+
+;;;###autoload
+(defun gh-get-labels ()
+ "Return a list of labels in the current GitHub repository."
+ (with-temp-buffer
+ (call-process "gh" nil t nil "label" "list"
+ "--sort" "name" "--json" "name" "--limit" "1000000")
+ (goto-char (point-min))
+ (seq-map (lambda (x) (gethash "name" x))
+ (json-parse-buffer))))
+
+;; TODO: Set title and body in a buffer like Magit
+;;;###autoload
+(defun gh-create-pr (title &optional labels draftp)
+ "Create a GitHub pull request.
+If DRAFTP is non-nil, the PR will be created as a draft.
+
+LABELS is a list of labels. A list of available labels can be fetched
+via `gh-get-labels'."
+ (declare (interactive-only t))
+ (interactive
+ (list
+ (read-string (format-prompt "PR Title" nil))
+ (completing-read-multiple (format-prompt "PR Labels" nil)
+ (gh-get-labels))
+ (y-or-n-p "Create PR as a draft? ")))
+ (let* ((project (project-name (project-current)))
+ (flags `("--fill-verbose" "--assignee" "@me"))
+ (label-string (mapconcat #'identity labels ",")))
+ ;; TODO: Remove this
+ (when (string= project "blixem")
+ (setq title (format "%s %s" (car (vc-git-branches)) title))
+ (when (member "Patch" labels)
+ (setq flags (append flags '("--base" "release")))))
+ (setq flags (append flags `("--title" ,title)))
+ (when draftp
+ (setq flags (append flags '("--draft"))))
+ (when labels
+ (setq flags (append flags `("--label" ,label-string))))
+ (with-temp-buffer
+ (apply #'call-process "gh" nil t nil "pr" "create" flags)
+ (message (buffer-string)))))
+
+;;;###autoload
+(defun gh-open-previous-pr ()
+ "Open the previous GitHub pull request.
+Opens the previous pull request created by `gh-create-pr' by searching
+for the echoed URL in the `*Messages*' buffer."
+ (interactive)
+ (with-current-buffer "*Messages*"
+ (goto-char (point-max))
+ (while (not (gh--pr-link-p (buffer-substring-no-properties
+ (pos-bol) (pos-eol))))
+ (unless (line-move -1 :noerror)
+ (user-error "No previous pull request found.")))
+ (browse-url-at-point)))
+
+(provide 'gh)
diff --git a/.config/emacs/site-lisp/grab.el b/.config/emacs/site-lisp/grab.el
new file mode 100644
index 0000000..f136376
--- /dev/null
+++ b/.config/emacs/site-lisp/grab.el
@@ -0,0 +1,215 @@
+;;; grab.el --- Emacs integration for grab -*- lexical-binding: t; -*-
+
+;; Author: Thomas Voss <mail@thomasvoss.com>
+;; Description: TODO
+;; Keywords: matching, tools
+
+;;; Commentary:
+
+;; TODO
+
+;;; Code:
+
+(require 'ansi-color)
+(require 'dired)
+(require 'project)
+(require 'rx)
+(require 'xref)
+
+(defgroup grab nil
+ "Settings for `grab'."
+ :group 'tools)
+
+(defcustom grab-command "grab"
+ "The base executable for the Grab tool."
+ :type 'string)
+
+(defcustom git-grab-command "git-grab"
+ "The base executable for the Git Grab tool."
+ :type 'string)
+
+(defcustom grab-command-arguments '("-c" "-Hmulti")
+ "Arguments to pass to `grab-command'."
+ :type '(repeat string))
+
+(defcustom git-grab-command-arguments grab-command-arguments
+ "Arguments to pass to `git-grab-command'."
+ :type '(repeat string))
+
+(defcustom grab-default-pattern '("x// h//" . 3)
+ "The default pattern in Grab prompts"
+ :type '(choice (cons string natnum)
+ (string)))
+
+(defvar grab-history nil
+ "Minibuffer history for Grab search patterns.")
+
+
+;;; Xref Location Class
+
+(cl-defstruct grab-location
+ "A location in a file specified by a byte offset."
+ file offset)
+
+(cl-defmethod xref-location-marker ((loc grab-location))
+ "Return a marker for the grab location LOC."
+ (let* ((file (grab-location-file loc))
+ (offset (grab-location-offset loc))
+ (buf (find-file-noselect file)))
+ (with-current-buffer buf
+ (save-restriction
+ (widen)
+ (goto-char (byte-to-position (1+ offset)))
+ (point-marker)))))
+
+(cl-defmethod xref-location-group ((loc grab-location))
+ "Group matches by their file name in the xref buffer."
+ (grab-location-file loc))
+
+(cl-defmethod xref-location-line ((loc grab-location))
+ "Return the position of the match.
+
+`xref' internally performs a log on this value, so we need to handle the
+0 case."
+ (max 1 (grab-location-offset loc)))
+
+
+;;; Process Management & Parsing
+
+(defvar grab--header-regexp
+ (rx-let ((ansi-escape (seq "\e[" (* (any "0-9;")) "m"))
+ (highlighted (thing)
+ (seq (* ansi-escape)
+ thing
+ (* ansi-escape))))
+ (rx line-start
+ (highlighted (group (+ (not (any ?: ?\e ?\n)))))
+ (highlighted ?:)
+ (highlighted (group (+ digit)))
+ (highlighted ?:)))
+ "Regular expression matching the grab output header.")
+
+(defun grab--format-summary (summary)
+ (let* ((summary (ansi-color-apply (string-trim-right summary)))
+ (pos 0)
+ (len (length summary)))
+ (while (< pos len)
+ (let ((next (next-property-change pos summary len)))
+ (when (or (get-text-property pos 'font-lock-face summary)
+ (get-text-property pos 'face summary))
+ (put-text-property pos next 'font-lock-face 'xref-match summary)
+ (remove-list-of-text-properties pos next '(face) summary))
+ (setq pos next)))
+ summary))
+
+(defun grab--parse-output (dir)
+ (let (xrefs file offset match-start)
+ (goto-char (point-min))
+ (while (re-search-forward grab--header-regexp nil :noerror)
+ (let ((next-file (match-string-no-properties 1))
+ (next-offset (string-to-number (match-string-no-properties 2)))
+ (next-start (point)))
+ (when file
+ (let* ((summary (buffer-substring-no-properties
+ match-start (match-beginning 0)))
+ (summary (grab--format-summary summary))
+ (full-path (expand-file-name file dir))
+ (loc (make-grab-location :file full-path :offset offset)))
+ (push (xref-make summary loc) xrefs)))
+ (setq file next-file
+ offset next-offset
+ match-start next-start)))
+ (when file
+ (let* ((summary (buffer-substring-no-properties
+ match-start (point-max)))
+ (summary (grab--format-summary summary))
+ (full-path (expand-file-name file dir))
+ (loc (make-grab-location :file full-path :offset offset)))
+ (push (xref-make summary loc) xrefs)))
+ (unless xrefs
+ (user-error "No matches found for grab pattern"))
+ (nreverse xrefs)))
+
+(defun grab--directory (cmd args pattern dir)
+ (grab--files cmd args pattern dir
+ (directory-files-recursively dir "." nil t)))
+
+(defun grab--files (cmd args pattern dir files)
+ (lambda ()
+ (let ((default-directory dir))
+ (with-temp-buffer
+ (apply #'call-process cmd nil t nil
+ (flatten-tree (list args "--" pattern files)))
+ (grab--parse-output dir)))))
+
+(defun grab--read-pattern ()
+ (read-string (format-prompt "Grab Pattern" nil)
+ grab-default-pattern
+ 'grab-history))
+
+
+;;; Interactive Commands
+
+;;;###autoload
+(defun grab (pattern)
+ "Run grab with PATTERN in the current directory."
+ (interactive (list (grab--read-pattern)))
+ (xref-show-xrefs
+ (grab--directory grab-command
+ grab-command-arguments
+ pattern
+ default-directory)
+ nil))
+
+;;;###autoload
+(defun git-grab (pattern)
+ "Run git grab with PATTERN in the current directory."
+ (interactive (list (grab--read-pattern)))
+ (xref-show-xrefs
+ (grab--files git-grab-command
+ git-grab-command-arguments
+ pattern
+ default-directory
+ nil)
+ nil))
+
+;;;###autoload
+(defun project-grab (pattern)
+ "Run grab with PATTERN at the project root."
+ (interactive (list (grab--read-pattern)))
+ (let* ((project (project-current t))
+ (default-directory (project-root project)))
+ (xref-show-xrefs
+ (grab--directory grab-command
+ grab-command-arguments
+ pattern
+ default-directory)
+ nil)))
+
+;;;###autoload
+(defun project-git-grab (pattern)
+ "Run git grab with PATTERN at the project root."
+ (interactive (list (grab--read-pattern)))
+ (let* ((project (project-current t))
+ (default-directory (project-root project)))
+ (xref-show-xrefs
+ (grab--files git-grab-command
+ git-grab-command-arguments
+ pattern
+ default-directory
+ nil)
+ nil)))
+
+;;;###autoload
+(defun dired-grab-marked-files (pattern)
+ "Run grab with PATTERN on all marked files in dired."
+ (interactive (list (grab--read-pattern)))
+ (let* ((project (project-current t))
+ (default-directory (project-root project)))
+ (xref-show-xrefs
+ (grab--files grab-command grab-command-arguments pattern
+ default-directory (dired-get-marked-files))
+ nil)))
+
+(provide 'grab)
+;;; grab.el ends here
diff --git a/.config/emacs/site-lisp/highlighter.el b/.config/emacs/site-lisp/highlighter.el
new file mode 100644
index 0000000..5b88182
--- /dev/null
+++ b/.config/emacs/site-lisp/highlighter.el
@@ -0,0 +1,132 @@
+;;; highlighter.el --- In-buffer highlighting commands -*- lexical-binding: t; -*-
+
+;; TODO: Support multiple cursors
+
+(eval-when-compile
+ (require 'seq))
+
+(defgroup highlighter nil
+ "Customization group for `highlighter'."
+ :group 'convenience)
+
+(defcustom highlighter-default-face 'match
+ "The default face used by `highlighter-mark'."
+ :type 'face
+ :package-version '(highlighter . "1.0.0")
+ :group 'highlighter)
+
+(defun highlighter-mark (arg)
+ "Highlight text in the buffer.
+Highlight the current line or region if it is active. Text is
+highlighted using the face specified by `highlighter-default-face'.
+
+With ARG, interactively pick a face to highlight with."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((bounds (if (use-region-p)
+ (region-bounds)
+ `((,(pos-bol) . ,(pos-eol)))))
+ (face (when arg
+ (highlighter--read-face-name "Highlight with face" #'facep))))
+ (highlighter-mark-region bounds face))
+ (when (region-active-p)
+ (deactivate-mark)))
+
+(defun highlighter-unmark (arg)
+ "Remove highlights in the buffer.
+
+Remove highlights from the current line or region if it is active.
+
+With ARG, interactively pick a face. Only highlights using the chosen
+face will be removed."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((bounds (if (use-region-p)
+ (region-bounds)
+ `((,(pos-bol) . ,(pos-eol)))))
+ (face (when arg
+ (highlighter--read-face-name
+ "Clear highlights using face"
+ #'highlighter--used-face-p))))
+ (highlighter-unmark-region bounds face))
+ (when (region-active-p)
+ (deactivate-mark)))
+
+(defun highlighter-mark-region (bounds &optional face)
+ "Highlight text in the buffer within BOUNDS.
+BOUNDS uses the same format as returned by `region-bounds'.
+
+Text is highlighted using the face specified by
+`highlighter-default-face'.
+
+If FACE is nil or omitted, `highlighter-default-face' is used."
+ (dolist (x bounds) (highlighter--mark-region (car x) (cdr x) face)))
+
+(defun highlighter-unmark-region (bounds &optional face)
+ "Remove highlights in the buffer within BOUNDS.
+BOUNDS uses the same format as returned by `region-bounds'.
+
+If FACE is non-nil, only remove highlights using FACE."
+ (dolist (x bounds) (highlighter--unmark-region (car x) (cdr x) face)))
+
+(defun highlighter--mark-region (beg end &optional face)
+ (let ((ov (make-overlay beg end nil :front-advance))
+ (face (or face highlighter-default-face 'match)))
+ (overlay-put ov 'priority 1)
+ (overlay-put ov 'face face)
+ (overlay-put ov 'evaporate t)
+ (overlay-put ov 'highlighter--mark-p t)
+ (overlay-put ov 'highlighter--face face)))
+
+(defun highlighter--unmark-region (beg end &optional face)
+ (if face
+ (remove-overlays beg end 'highlighter--face face)
+ (remove-overlays beg end 'highlighter--mark-p t)))
+
+(defun highlighter-unmark-buffer (arg)
+ "Remove highlights in the buffer.
+
+With ARG, interactively pick a face. Only highlights using the chosen
+face will be removed."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((face (when arg
+ (highlighter--read-face-name
+ "Clear highlights using face"
+ #'highlighter--used-face-p))))
+ (highlighter--unmark-region (point-min) (point-max) face)))
+
+(defun highlighter--read-face-name (prompt face-predicate)
+ (let (default defaults)
+ (let ((prompt (format "%s: " prompt))
+ (completion-extra-properties
+ `(:affixation-function
+ ,(lambda (faces)
+ (mapcar
+ (lambda (face)
+ (list face
+ (concat (propertize read-face-name-sample-text
+ 'face face)
+ "\t")
+ ""))
+ faces))))
+ aliasfaces nonaliasfaces faces)
+ ;; Build up the completion tables.
+ (mapatoms (lambda (s)
+ (when (apply face-predicate s nil)
+ (if (get s 'face-alias)
+ (push (symbol-name s) aliasfaces)
+ (push (symbol-name s) nonaliasfaces)))))
+ (let ((face (completing-read
+ prompt
+ (completion-table-in-turn nonaliasfaces aliasfaces)
+ nil t nil 'face-name-history defaults)))
+ (when (facep face) (if (stringp face)
+ (intern face)
+ face))))))
+
+(defun highlighter--used-face-p (face)
+ (seq-filter (lambda (ov) (eq face (overlay-get ov 'highlighter--face)))
+ (overlays-in (point-min) (point-max))))
+
+(provide 'highlighter)
diff --git a/.config/emacs/site-lisp/html-escape.el b/.config/emacs/site-lisp/html-escape.el
new file mode 100644
index 0000000..54a8d34
--- /dev/null
+++ b/.config/emacs/site-lisp/html-escape.el
@@ -0,0 +1,56 @@
+;;; html-escape.el --- HTML escaping functions -*- lexical-binding: t; -*-
+
+(defgroup html-escape nil
+ "Customization group for `html-escape'."
+ :group 'convenience)
+
+(defvar html-escape-table
+ (let ((table (make-hash-table :test #'eq)))
+ (puthash ?& "&amp;" table)
+ (puthash ?< "&lt;" table)
+ (puthash ?> "&gt;" table)
+ (puthash ?\" "&quot;" table)
+ (puthash ?' "&#39;" table)
+ table)
+ "Hash table mapping character codes to their HTML entity equivalents.")
+
+;;;###autoload
+(defun html-escape ()
+ "HTML escape text in the current buffer.
+
+Perform HTML escaping on the text in the current buffer. If the
+region is active then only escape the contents of the active region."
+ (declare (interactive-only t))
+ (interactive "*")
+ (if (use-region-p)
+ (html-escape-region (region-bounds))
+ (html-escape-region-1 (pos-bol) (pos-eol)))
+ (when (region-active-p)
+ (deactivate-mark)))
+
+(defun html-escape-region (bounds)
+ "HTML escape text in the current buffer within BOUNDS.
+
+BOUNDS takes the same form as the return value of `region-bounds'.
+This function is prefered as it supports noncontiguous regions, but
+there also exists `html-escape-region-1' with a simpler bounds
+interface."
+ (dolist (x bounds) (html-escape-region-1 (car x) (cdr x))))
+
+(defun html-escape-region-1 (beg end)
+ "HTML escape text in the current buffer within BEG and END.
+
+This function is the same as the prefered `html-escape-region', but
+takes BEG and END parameters instead of a BOUNDS parameter. For
+noncontiguous region support use `html-escape-region'."
+ (save-restriction
+ (narrow-to-region beg end)
+ (save-excursion
+ (goto-char (point-min))
+ (save-match-data
+ (while (re-search-forward "[&<>\"']" nil :noerror)
+ (let* ((char (char-after (match-beginning 0)))
+ (replacement (gethash char html-escape-table)))
+ (replace-match replacement)))))))
+
+(provide 'html-escape)
diff --git a/.config/emacs/site-lisp/increment.el b/.config/emacs/site-lisp/increment.el
new file mode 100644
index 0000000..b1a69ae
--- /dev/null
+++ b/.config/emacs/site-lisp/increment.el
@@ -0,0 +1,132 @@
+;;; increment.el -- Increment numbers at point -*- lexical-binding: t; -*-
+
+(require 'cl-macs)
+(require 'rx)
+
+(defvar increment--binary-number-regexp
+ (rx (group (or ?- word-start))
+ "0b"
+ (group (* ?0))
+ (group (+ (any "01")))
+ word-end))
+
+(defvar increment--octal-number-regexp
+ (rx (group (or ?- word-start))
+ "0o"
+ (group (* ?0))
+ (group (+ (any "0-7")))
+ word-end))
+
+(defvar increment--decimal-number-regexp
+ (rx (group (? ?-))
+ (group (* ?0))
+ (group (+ (any digit)))))
+
+(defvar increment--hexadecimal-number-regexp
+ (rx (group (or ?- word-start))
+ "0x"
+ (group (* ?0))
+ (group (+ (any hex-digit)))
+ word-end))
+
+(defvar increment--hexadecimal-lower-number-regexp
+ (rx (group (or ?- word-start))
+ "0x"
+ (group (* ?0))
+ (group (+ (any "0-9a-f")))
+ word-end))
+
+(defvar increment--number-regexp
+ (rx (or (seq (or ?- word-start)
+ (or (seq "0b" (+ (any "01")))
+ (seq "0o" (+ (any "0-7")))
+ (seq "0x" (+ (any hex-digit))))
+ word-end)
+ (seq (? ?-) (+ (any digit))))))
+
+(defun increment--number-to-binary-string (number)
+ (nreverse
+ (cl-loop for x = number then (ash x -1)
+ while (not (= x 0))
+ concat (if (= 0 (logand x 1)) "0" "1"))))
+
+(defun increment--format-number-with-base
+ (number base leading-zeros buffer-substr hex-style)
+ (let* ((neg (> 0 number))
+ (number (abs number))
+ (number-string
+ (pcase base
+ (2 (increment--number-to-binary-string number))
+ (8 (format "%o" number))
+ (10 (number-to-string number))
+ (16 (format (if (eq hex-style 'lower) "%x" "%X") number))))
+ (length-diff (- (length buffer-substr)
+ (length number-string)))
+ (leading-zeros (if (> leading-zeros 0)
+ (+ leading-zeros length-diff)
+ 0)))
+ (concat
+ (when neg
+ "-")
+ (pcase base
+ (2 "0b")
+ (8 "0o")
+ (16 "0x"))
+ (when (> leading-zeros 0)
+ (make-string leading-zeros ?0))
+ number-string)))
+
+(defun increment--match-number-at-point ()
+ (cond ((thing-at-point-looking-at
+ increment--binary-number-regexp)
+ (cons 2 nil))
+ ((thing-at-point-looking-at
+ increment--octal-number-regexp)
+ (cons 8 nil))
+ ((thing-at-point-looking-at
+ increment--hexadecimal-number-regexp)
+ (cons 16 nil))
+ ((thing-at-point-looking-at
+ increment--hexadecimal-lower-number-regexp)
+ (cons 16 'lower))
+ ((thing-at-point-looking-at
+ increment--decimal-number-regexp)
+ (cons 10 nil))))
+
+;;;###autoload
+(cl-defun increment-number-at-point (&optional arg)
+ "Increment the number at point by ARG or 1 if ARG is nil. If called
+interactively, the universal argument can be used to specify ARG. If
+the number at point has leading zeros then the width of the number is
+preserved."
+ (interactive "*p")
+ (save-match-data
+ (let (case-fold-search
+ (match-pair (increment--match-number-at-point)))
+ (unless match-pair
+ (let ((save-point (point)))
+ (unless (re-search-forward
+ increment--number-regexp
+ (line-end-position) :noerror)
+ (goto-char save-point)
+ (cl-return-from increment-number-at-point))
+ (setq match-pair (increment--match-number-at-point))))
+ (let* ((base (car match-pair))
+ (hex-style (cdr match-pair))
+ (substr (buffer-substring-no-properties
+ (match-beginning 3) (match-end 3)))
+ (sign (if (= (match-beginning 1) (match-end 1)) +1 -1))
+ (result (+ (* (string-to-number substr base) sign)
+ (or arg 1))))
+ (replace-match
+ (increment--format-number-with-base
+ result base (- (match-end 2) (match-beginning 2))
+ substr hex-style))))))
+
+;;;###autoload
+(defun decrement-number-at-point (&optional arg)
+ "The same as `increment-number-at-point', but ARG is negated."
+ (interactive "*p")
+ (increment-number-at-point (- (or arg 1))))
+
+(provide 'increment)
diff --git a/.config/emacs/site-lisp/jq.el b/.config/emacs/site-lisp/jq.el
new file mode 100644
index 0000000..0a1c0eb
--- /dev/null
+++ b/.config/emacs/site-lisp/jq.el
@@ -0,0 +1,115 @@
+;;; jq.el --- Interact with JQ in Emacs -*- lexical-binding: t; -*-
+
+(eval-when-compile
+ (require 'cl-lib))
+
+(defgroup jq nil
+ "Interact with JQ within Emacs."
+ :group 'tools
+ :prefix "jq-")
+
+(defcustom jq-live-major-mode
+ (cond ((fboundp #'json-ts-mode) #'json-ts-mode)
+ ((fboundp #'js-json-mode) #'js-json-mode))
+ "The major mode to use for display JSON with `jq-live'."
+ :type 'function
+ :group 'jq)
+
+(defun jq--run (query json-input tabsp)
+ "Run jq QUERY on JSON-INPUT string."
+ (with-temp-buffer
+ (insert json-input)
+ (let ((args (list query)))
+ (when tabsp
+ (push "--tab" args))
+ (let ((exit-code (apply #'call-process-region (point-min) (point-max)
+ "jq" :delete t nil args)))
+ (cons exit-code (buffer-string))))))
+
+(defun jq-live--render-preview (query json-input preview-buffer tabsp)
+ "Render the live JQ preview into PREVIEW-BUFFER."
+ (let ((inhibit-read-only t))
+ (with-current-buffer preview-buffer
+ (erase-buffer)
+ (condition-case err
+ (cl-destructuring-bind (exit-code . string)
+ (jq--run query json-input tabsp)
+ (if (zerop exit-code)
+ (insert string)
+ (insert (propertize string 'face 'error))
+ (insert json-input)))
+ (error
+ (insert (format "%s\n%s"
+ (propertize (format "Error: %s" err) 'face 'error)
+ json-input))))
+ (goto-char (point-min))
+ (when (fboundp jq-live-major-mode)
+ (funcall jq-live-major-mode))))
+ (display-buffer preview-buffer))
+
+;;;###autoload
+(defun jq-filter-region (query &optional beg end)
+ "Filter the region between BEG and END with a jq QUERY.
+When called interactively, QUERY is read from the minibuffer, and the
+active region is filtered. If there is no active region, the whole
+buffer is filtered.
+
+For interactive filtering, see `jq-live'."
+ (interactive
+ (list
+ (read-string (format-prompt "Query" nil))
+ (when (use-region-p) (region-beginning))
+ (when (use-region-p) (region-end))))
+ (let* ((beg (or beg (point-min)))
+ (end (or end (point-max)))
+ (json-input (buffer-substring-no-properties beg end))
+ (tabsp indent-tabs-mode))
+ (cl-destructuring-bind (exit-code . output)
+ (jq--run query json-input tabsp)
+ (if (zerop exit-code)
+ (atomic-change-group
+ (delete-region beg end)
+ (insert output)
+ (indent-region beg (point))
+ (message "jq applied."))
+ (message "jq error: %s" output)))))
+
+;;;###autoload
+(defun jq-live (&optional beg end)
+ "Filter the region between BEG and END with a live preview.
+For non-interactive filtering, see `jq-filter-region'."
+ (declare (interactive-only t))
+ (interactive
+ (list (when (use-region-p) (region-beginning))
+ (when (use-region-p) (region-end))))
+ (unless (executable-find "jq")
+ (user-error "`jq' not found in PATH."))
+
+ (let* ((input-buffer (current-buffer))
+ (beg (or beg (point-min)))
+ (end (or end (point-max)))
+ (json-input (buffer-substring-no-properties beg end))
+ (tabsp indent-tabs-mode)
+ (preview-buffer-name (format "*JQ Preview: %s*" (buffer-name)))
+ (preview-buffer (get-buffer-create preview-buffer-name))
+ (last-query "")
+ (update-fn
+ (lambda ()
+ (let ((query (minibuffer-contents)))
+ (unless (equal query last-query)
+ (setq last-query query)
+ (jq-live--render-preview query json-input preview-buffer
+ tabsp))))))
+
+ (unwind-protect
+ (let ((query
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (add-hook 'post-command-hook update-fn nil :local))
+ (read-from-minibuffer (format-prompt "Query" nil)))))
+ (with-current-buffer input-buffer
+ (jq-filter-region query beg end)))
+ (when (buffer-live-p preview-buffer)
+ (kill-buffer preview-buffer)))))
+
+(provide 'jq)
diff --git a/.config/emacs/site-lisp/mm-lib.el b/.config/emacs/site-lisp/mm-lib.el
new file mode 100644
index 0000000..e128569
--- /dev/null
+++ b/.config/emacs/site-lisp/mm-lib.el
@@ -0,0 +1,80 @@
+;;; mm-lib.el --- Helper functions and macros -*- lexical-binding: t; -*-
+
+(defun mm-mode-to-hook (mode)
+ "Get the hook corresponding to MODE."
+ (declare (ftype (function (symbol) symbol))
+ (pure t) (side-effect-free t))
+ (intern (concat (symbol-name mode) "-hook")))
+
+(defun mm-mode-to-ts-mode (mode)
+ "Get the Tree-Sitter mode corresponding to MODE."
+ (declare (ftype (function (symbol) symbol))
+ (pure t) (side-effect-free t))
+ (intern (concat
+ (string-remove-suffix "-mode" (symbol-name mode))
+ "-ts-mode")))
+
+(defun mm-ts-mode-to-mode (ts-mode)
+ "Get the non-Tree-Sitter mode corresponding to TS-MODE."
+ (declare (ftype (function (symbol) symbol))
+ (pure t) (side-effect-free t))
+ (intern (concat
+ (string-remove-suffix "-ts-mode" (symbol-name ts-mode))
+ "-mode")))
+
+(defsubst mm-string-split (separators string)
+ "Split STRING on SEPARATORS.
+Wrapper around `string-split' that puts separators first. This makes it
+convenient to use in `thread-last'."
+ (declare (ftype (function (string string) (list string)))
+ (pure t) (side-effect-free t))
+ (string-split string separators))
+
+(defun mm-as-number (string-or-number)
+ "Ensure STRING-OR-NUMBER is a number.
+If given a number return STRING-OR-NUMBER as-is, otherwise convert it to
+a number and then return it.
+
+This function is meant to be used in conjuction with `read-string' and
+`format-prompt'."
+ (declare (ftype (function (or string number) number))
+ (pure t) (side-effect-free t))
+ (if (stringp string-or-number)
+ (string-to-number string-or-number)
+ string-or-number))
+
+(defun mm-camel-to-lisp (string)
+ "Convert STRING from camelCase to lisp-case."
+ (declare (ftype (function (string) string))
+ (pure t) (side-effect-free t))
+ (let ((case-fold-search nil))
+ (downcase
+ (replace-regexp-in-string
+ (rx (group (or lower digit)) (group upper)) "\\1-\\2" string))))
+
+(defun mm-do-and-center (function &rest arguments)
+ "Call FUNCTION with ARGUMENTS and then center the screen."
+ (apply function arguments)
+ (when (called-interactively-p)
+ (recenter)))
+
+(defmacro mm-comment (&rest _body)
+ "Comment out BODY.
+A cleaner alternative to line-commenting a region."
+ (declare (indent 0))
+ nil)
+
+(defun mm-nil (&rest _)
+ "Return nil."
+ nil)
+
+(defmacro mm-with-suppressed-output (&rest body)
+ "Execute BODY while suppressing output.
+Execute BODY as given with all output to the echo area or the *Messages*
+buffer suppressed."
+ (declare (indent 0))
+ `(let ((inhibit-message t)
+ (message-log-max nil))
+ ,@body))
+
+(provide 'mm-lib)
diff --git a/.config/emacs/site-lisp/multiple-cursors-extensions.el b/.config/emacs/site-lisp/multiple-cursors-extensions.el
new file mode 100644
index 0000000..7a897fc
--- /dev/null
+++ b/.config/emacs/site-lisp/multiple-cursors-extensions.el
@@ -0,0 +1,123 @@
+;;; multiple-cursors-extensions.el --- Extensions to multiple-cursors.el -*- lexical-binding: t; -*-
+
+;;; Helper functions
+(defun mce--rotate-left (n list)
+ "Rotate the elements of LIST N places to the left."
+ (declare (ftype (function (number (list t)) (list t)))
+ (pure t) (side-effect-free t))
+ (append (nthcdr n list) (butlast list (- (length list) n))))
+
+(defun mce--rotate-right (n list)
+ "Rotate the elements of LIST N places to the right."
+ (declare (ftype (function (number (list t)) (list t)))
+ (pure t) (side-effect-free t))
+ (mce--rotate-left (- (length list) n) list))
+
+(defmacro mce--define-marking-command (name search-function noun)
+ (let ((noun-symbol (intern noun)))
+ `(defun ,name (beg end ,noun-symbol)
+ ,(format "Mark all occurances of %s between BEG and END.
+If called interactively with an active region then all matches in the
+region are marked, otherwise all matches in the buffer are marked."
+ (upcase noun))
+ (interactive
+ (list (or (use-region-beginning) (point-min))
+ (or (use-region-end) (point-max))
+ (read-string
+ (format-prompt ,(concat "Match " noun) nil))))
+ (if (string-empty-p ,noun-symbol)
+ (message "Command aborted")
+ (catch 'mce--no-match
+ (mc/remove-fake-cursors)
+ (goto-char beg)
+ (let (did-match-p)
+ (while (,search-function ,noun-symbol end :noerror)
+ (setq did-match-p t)
+ (push-mark (match-beginning 0))
+ (exchange-point-and-mark)
+ (mc/create-fake-cursor-at-point)
+ (goto-char (mark)))
+ (unless did-match-p
+ (message "No match for `%s'" ,noun-symbol)
+ (throw 'mce--no-match nil)))
+ (when-let ((first (mc/furthest-cursor-before-point)))
+ (mc/pop-state-from-overlay first))
+ (multiple-cursors-mode (if (> (mc/num-cursors) 1)
+ 1
+ 0)))))))
+
+
+;;; Public API
+
+(defun mce-transpose-cursor-regions (n)
+ "Interchange the regions of each cursor.
+With prefix arg N, the regions are rotated N places (backwards if N is
+negative)."
+ (interactive "*p")
+ (when (= (mc/num-cursors) 1)
+ (user-error "Cannot transpose with only one cursor."))
+ (unless (use-region-p)
+ (user-error "No active region."))
+ (setq mc--strings-to-replace
+ (funcall (if (< n 0)
+ #'mce--rotate-left
+ #'mce--rotate-right)
+ (abs n)
+ (mc--ordered-region-strings)))
+ (mc--replace-region-strings))
+
+(mce--define-marking-command mce-mark-all-in-region
+ search-forward
+ "string")
+(mce--define-marking-command mce-mark-all-in-region-regexp
+ re-search-forward
+ "regexp")
+
+(defun mce-add-cursor-to-next-thing (thing)
+ "Add a fake cursor to the next occurance of THING.
+THING is any symbol that can be given to `bounds-of-thing-at-point'.
+
+If there is an active region, the next THING will be marked."
+ (let ((bounds (bounds-of-thing-at-point thing)))
+ (if (null bounds)
+ (progn
+ (forward-thing thing)
+ (goto-char (car (bounds-of-thing-at-point thing))))
+ (mc/save-excursion
+ (when (> (mc/num-cursors) 1)
+ (goto-char (overlay-end (mc/furthest-cursor-after-point))))
+ (goto-char (cdr (bounds-of-thing-at-point thing)))
+ (forward-thing thing)
+ (let ((bounds (bounds-of-thing-at-point thing)))
+ (goto-char (car bounds))
+ (when (use-region-p)
+ (push-mark (cdr bounds)))
+ (mc/create-fake-cursor-at-point))))))
+
+(defun mce-add-cursor-to-next-word ()
+ "Add a fake cursor to the next word."
+ (declare (interactive-only t))
+ (interactive)
+ (mce-add-cursor-to-next-thing 'word)
+ (mc/maybe-multiple-cursors-mode))
+
+(defun mce-add-cursor-to-next-symbol ()
+ "Add a fake cursor to the next symbol."
+ (declare (interactive-only t))
+ (interactive)
+ (mce-add-cursor-to-next-thing 'symbol)
+ (mc/maybe-multiple-cursors-mode))
+
+(defun emc-sort-regions (&optional reversep)
+ "Sort marked regions.
+This command is an exact replica of `mc/sort-regions' except that
+calling this command with a prefix argument REVERSE sorts the marked
+regions in reverse order."
+ (interactive "*P")
+ (unless (use-region-p)
+ (user-error "No active region."))
+ (setq mc--strings-to-replace (sort (mc--ordered-region-strings)
+ (if reversep #'string> #'string<)))
+ (mc--replace-region-strings))
+
+(provide 'multiple-cursors-extensions)
diff --git a/.config/emacs/site-lisp/number-format.el b/.config/emacs/site-lisp/number-format.el
new file mode 100644
index 0000000..06ce206
--- /dev/null
+++ b/.config/emacs/site-lisp/number-format.el
@@ -0,0 +1,129 @@
+;;; number-format-mode.el --- Format numbers in the current buffer -*- lexical-binding: t; -*-
+
+(eval-when-compile
+ (require 'cl-macs)
+ (require 'seq))
+
+(defgroup number-format nil
+ "Customization group for `number-format'."
+ :group 'convenience) ; TODO: Is this the right group?
+
+(defcustom number-format-separator "."
+ "Thousands separator to use in numeric literals."
+ :type 'string
+ :package-version '(number-format-mode . "1.0.0")
+ :group 'number-format)
+
+(defcustom number-format-predicate nil
+ "Function determining if a number should be formatted.
+When formatting a number, this function is called with the START and END
+range of the number in the buffer. If this function returns non-nil the
+number is formatted.
+
+If this function is nil then all numbers are formatted."
+ :type 'function
+ :package-version '(number-format-mode . "1.0.0")
+ :group 'number-format)
+
+(defvar-local number-format--overlays (make-hash-table :test 'eq))
+(defconst number-format--regexp "\\b[0-9]\\{4,\\}\\b")
+
+(defun number-format--add-separators (s)
+ (while (string-match "\\(.*[0-9]\\)\\([0-9][0-9][0-9].*\\)" s)
+ (setq s (concat (match-string 1 s)
+ number-format-separator
+ (match-string 2 s))))
+ s)
+
+(defun number-format--adjust-overlays (ov _1 beg end &optional _2)
+ (let* ((ov-beg (overlay-start ov))
+ (ov-end (overlay-end ov))
+ (overlays (overlays-in ov-beg ov-end)))
+ (mapcar #'delete-overlay (gethash ov number-format--overlays))
+ (save-excursion
+ (goto-char ov-beg)
+ (if (looking-at number-format--regexp :inhibit-modify)
+ (puthash ov (number-format--at-range ov-beg ov-end)
+ number-format--overlays)
+ (delete-overlay ov)
+ (remhash ov number-format--overlays)))))
+
+(defun number-format--at-range (beg end)
+ (when (or (null number-format-predicate)
+ (funcall number-format-predicate beg end))
+ (let* ((offsets [3 1 2])
+ (len (- end beg))
+ (off (aref offsets (mod len 3))))
+ (goto-char (+ beg off)))
+ (let (overlays)
+ (while (< (point) end)
+ (let* ((group-end (+ (point) 3))
+ (ov (make-overlay (point) group-end)))
+ (overlay-put ov 'before-string ".")
+ (overlay-put ov 'evaporate t)
+ (push ov overlays)
+ (goto-char group-end)))
+ overlays)))
+
+(defun number-format--jit-lock (beg end)
+ (let ((line-beg (save-excursion (goto-char beg) (line-beginning-position)))
+ (line-end (save-excursion (goto-char end) (line-end-position))))
+ (number-unformat-region line-beg line-end)
+ (number-format-region line-beg line-end)))
+
+;;;###autoload
+(defun number-format-region (beg end)
+ "Format numbers between BEG and END.
+When called interactively, format numbers in the active region."
+ (interactive "r")
+ (save-excursion
+ (goto-char beg)
+ (save-restriction
+ (narrow-to-region beg end)
+ (number-unformat-region beg end)
+ (while (re-search-forward number-format--regexp nil :noerror)
+ (save-excursion
+ (cl-destructuring-bind (beg end) (match-data)
+ (let ((ov (make-overlay beg end nil nil :rear-advance)))
+ (overlay-put ov 'evaporate t)
+ (dolist (sym '(insert-behind-hooks
+ insert-in-front-hooks
+ modification-hooks))
+ (overlay-put ov sym '(number-format--adjust-overlays)))
+ (puthash ov (number-format--at-range beg end)
+ number-format--overlays))))))))
+
+;;;###autoload
+(defun number-unformat-region (beg end)
+ "Unformat numbers between BEG and END.
+When called interactively, unformat numbers in the active region."
+ (interactive "r")
+ (dolist (ov (overlays-in beg end))
+ (when-let ((overlays (gethash ov number-format--overlays)))
+ (mapcar #'delete-overlay overlays)
+ (remhash ov number-format--overlays)
+ (delete-overlay ov))))
+
+;;;###autoload
+(defun number-format-buffer ()
+ "Format numbers in the current buffer."
+ (interactive)
+ (number-format-region (point-min) (point-max)))
+
+;;;###autoload
+(defun number-unformat-buffer ()
+ "Unformat numbers in the current buffer."
+ (interactive)
+ (number-unformat-region (point-min) (point-max)))
+
+;;;###autoload
+(define-minor-mode number-format-mode
+ "TODO"
+ :lighter " Number-Format"
+ :group 'number-format
+ (number-unformat-buffer)
+ (if number-format-mode
+ (jit-lock-register #'number-format--jit-lock)
+ (jit-lock-unregister #'number-format--jit-lock)))
+
+(provide 'number-format)
diff --git a/.config/emacs/themes/mango-dark-theme.el b/.config/emacs/themes/mango-dark-theme.el
new file mode 100644
index 0000000..07059d6
--- /dev/null
+++ b/.config/emacs/themes/mango-dark-theme.el
@@ -0,0 +1,250 @@
+;;; mango-dark-theme.el --- Just your average dark theme -*- lexical-binding: t; -*-
+
+(deftheme mango-dark
+ "Mildly dark, dark theme.
+Your average not-so-dark dark theme, because none of the other options
+were exactly to my liking. It’s about time I had a theme to call my
+own.")
+
+(defconst mango-dark-theme-colors-alist
+ '((foreground . ("#C5C8C6" "color-251" "white"))
+ (background . ("#2B303B" "color-236" "black"))
+ (background-cool . ("#363C4A" "color-237" "black"))
+ (background-dark . ("#1D2635" "color-234" "black"))
+ (background-faint . ("#414859" "color-238" "brightblack"))
+ (middleground . ("#4F5561" "color-239" "brightblack"))
+ (disabled . ("#999999" "color-246" "brightblack"))
+ (celestial-blue . ("#569CD6" "color-74" "brightblue"))
+ (dark-red . ("#841A11" "color-88" "red"))
+ (khaki . ("#F0E68C" "color-228" "yellow"))
+ (lime . ("#B8F182" "color-156" "green"))
+ (magenta . ("#ED97F5" "color-213" "magenta"))
+ (pale-azure . ("#9CDCFE" "color-117" "cyan"))
+ (red . ("#E60026" "color-160" "brightred"))
+ (salmon . ("#F1B282" "color-216" "brightyellow"))
+ (violet . ("#E57AE5" "color-176" "brightmagenta")))
+ "The color palette used throughout `mango-dark-theme'.
+Each color is mapped to a list of colors of the form
+(GUI-HEX 256-COLOR 16-COLOR) for use in true-color, 256-color, and
+16-color modes.")
+
+(defsubst mango-dark-theme-color (name &optional display)
+ "Get the color value of NAME for the given DISPLAY.
+DISPLAY can be 'gui, '256, or '16."
+ (let ((colors (alist-get name mango-dark-theme-colors-alist)))
+ (pcase display
+ ('gui (nth 0 colors))
+ ('256 (nth 1 colors))
+ ('16 (nth 2 colors))
+ (_ (nth 0 colors)))))
+
+(defun mango-dark-theme-spec (&rest props)
+ "Generate a tiered display specification list from PROPS.
+Values that match keys in `mango-dark-theme-colors-alist' are
+automatically mapped to their correct display colors."
+ (let (gui c256 c16)
+ (while props
+ (let ((key (pop props))
+ (val (pop props)))
+ (push key gui)
+ (push key c256)
+ (push key c16)
+ (if (and (symbolp val) (alist-get val mango-dark-theme-colors-alist))
+ (progn
+ (push (mango-dark-theme-color val 'gui) gui)
+ (push (mango-dark-theme-color val '256) c256)
+ (push (mango-dark-theme-color val '16) c16))
+ (push val gui)
+ (push val c256)
+ (push val c16))))
+ `((((type graphic tty) (min-colors #x1000000))
+ ,(nreverse gui))
+ (((type tty) (min-colors 256))
+ ,(nreverse c256))
+ (((type tty))
+ ,(nreverse c16)))))
+
+(custom-theme-set-faces
+ 'mango-dark
+
+ ;; Standard Stuff
+ `(default
+ ,(mango-dark-theme-spec
+ :foreground 'foreground
+ :background 'background))
+ `(fringe
+ ((t (:inherit default))))
+
+ ;; Lines
+ `(hl-line
+ ,(mango-dark-theme-spec
+ :background 'background-faint))
+ `(region
+ ,(mango-dark-theme-spec
+ :background 'middleground))
+ `(header-line
+ ,(mango-dark-theme-spec
+ :background 'middleground))
+ `(mode-line-active
+ ((t ( :box ,(mango-dark-theme-color 'foreground 'gui)
+ :inherit header-line))))
+ `(mode-line-inactive
+ ,(mango-dark-theme-spec
+ :background 'background-cool
+ :weight 'light))
+ `(window-divider
+ ,(mango-dark-theme-spec
+ :foreground 'background-cool))
+ `(window-divider-first-pixel
+ ,(mango-dark-theme-spec
+ :foreground 'background-cool))
+ `(window-divider-last-pixel
+ ,(mango-dark-theme-spec
+ :foreground 'background-cool))
+
+ ;; Line Numbers
+ `(line-number
+ ,(mango-dark-theme-spec
+ :foreground 'background-faint
+ :background 'background))
+ `(line-number-current-line
+ ,(mango-dark-theme-spec
+ :foreground 'salmon
+ :background 'background
+ :weight 'bold))
+
+ ;; Documentation
+ `(font-lock-comment-face
+ ,(mango-dark-theme-spec
+ :foreground 'khaki
+ :weight 'semi-bold))
+ `(font-lock-doc-face
+ ,(mango-dark-theme-spec
+ :foreground 'disabled))
+
+ ;; Modeline
+ `(mm-modeline-overwrite-face
+ ((t (:weight bold))))
+ `(mm-modeline-readonly-face
+ ((t (:weight bold))))
+ `(mm-modeline-buffer-name-face
+ ((t (:inherit font-lock-constant-face))))
+ `(mm-modeline-buffer-modified-face
+ ((t (:inherit shadow))))
+ `(mm-modeline-major-mode-name-face
+ ((t (:weight bold))))
+ `(mm-modeline-major-mode-symbol-face
+ ((t (:inherit shadow))))
+ `(mm-modeline-git-branch-face
+ ((t (:inherit font-lock-constant-face))))
+ `(mm-modeline-narrow-face
+ ,(mango-dark-theme-spec
+ :background 'dark-red
+ :box 'dark-red
+ :weight 'bold))
+
+ ;; Core Language
+ `(font-lock-builtin-face
+ ((t (:inherit font-lock-preprocessor-face))))
+ `(font-lock-keyword-face
+ ,(mango-dark-theme-spec
+ :foreground 'violet))
+ `(font-lock-type-face
+ ,(mango-dark-theme-spec
+ :foreground 'celestial-blue))
+
+ ;; Function-likes
+ `(font-lock-function-name-face
+ ,(mango-dark-theme-spec
+ :foreground 'khaki))
+ `(font-lock-preprocessor-face
+ ,(mango-dark-theme-spec
+ :foreground 'magenta
+ :weight 'bold))
+
+ ;; Variables
+ `(font-lock-constant-face
+ ((t ( :inherit font-lock-variable-name-face
+ :weight bold))))
+ `(font-lock-variable-name-face
+ ,(mango-dark-theme-spec
+ :foreground 'pale-azure))
+
+ ;; Other literals
+ `(help-key-binding
+ ((t (:inherit font-lock-constant-face))))
+ `(font-lock-number-face
+ ,(mango-dark-theme-spec
+ :foreground 'salmon))
+
+ ;; Org Mode
+ `(org-quote
+ ((t ( :inherit org-block
+ :slant italic))))
+ `(org-code
+ ,(mango-dark-theme-spec
+ :foreground 'salmon))
+ `(org-verbatim
+ ,(mango-dark-theme-spec
+ :foreground 'lime))
+ `(org-block
+ ,(mango-dark-theme-spec
+ :background 'background-cool))
+ `(org-hide
+ ,(mango-dark-theme-spec
+ :foreground 'background))
+
+ ;; Info Page
+ `(Info-quoted
+ ((t (:inherit default))))
+
+ ;; Magit
+ `(magit-diff-context-highlight
+ ((t (:inherit hl-line))))
+ `(magit-section-highlight
+ ((t (:inherit hl-line))))
+ `(magit-diff-hunk-heading
+ ,(mango-dark-theme-spec
+ :background 'background-cool))
+ `(magit-diff-hunk-heading-highlight
+ ,(mango-dark-theme-spec
+ :background 'middleground))
+ `(git-commit-summary
+ ,(mango-dark-theme-spec
+ :foreground 'khaki))
+ `(git-commit-overlong-summary
+ ,(mango-dark-theme-spec
+ :foreground 'foreground
+ :background 'red
+ :weight 'bold))
+
+ ;; Vertico
+ `(vertico-current ((t (:inherit hl-line))))
+
+ ;; Marginalia
+ `(mm-diffstat-counter-added
+ ((t ( :foreground "green"
+ :weight bold))))
+ `(mm-diffstat-counter-removed
+ ((t ( :foreground "red"
+ :weight bold))))
+ `(marginalia-documentation
+ ,(mango-dark-theme-spec
+ :foreground 'disabled
+ :underline nil))
+
+ ;; Tempel
+ `(tempel-default
+ ,(mango-dark-theme-spec
+ :slant 'italic
+ :background 'middleground))
+ `(tempel-field
+ ,(mango-dark-theme-spec
+ :slant 'italic
+ :background 'middleground))
+ `(tempel-form
+ ,(mango-dark-theme-spec
+ :slant 'italic
+ :background 'middleground)))
+
+(provide-theme 'mango-dark)
diff --git a/.config/emacs/themes/mango-light-theme.el b/.config/emacs/themes/mango-light-theme.el
new file mode 100644
index 0000000..693a0b3
--- /dev/null
+++ b/.config/emacs/themes/mango-light-theme.el
@@ -0,0 +1,250 @@
+;;; mango-light-theme.el --- Just your average light theme -*- lexical-binding: t; -*-
+
+(deftheme mango-light
+ "Mildly light, light theme.
+Your average not-so-light light theme, because none of the other options
+were exactly to my liking. It’s about time I had a theme to call my
+own.")
+
+(defconst mango-light-theme-colors-alist
+ '((foreground . ("#3B4252" "color-238" "black"))
+ (background . ("#ECEFF4" "color-255" "white"))
+ (background-cool . ("#E5E9F0" "color-254" "white"))
+ (background-dark . ("#FAFBFC" "color-231" "brightwhite"))
+ (background-faint . ("#D8DEE9" "color-253" "brightwhite"))
+ (middleground . ("#C8D0E0" "color-252" "brightwhite"))
+ (disabled . ("#9BA6B5" "color-247" "brightblack"))
+ (celestial-blue . ("#1B61CE" "color-26" "blue"))
+ (dark-red . ("#A12027" "color-124" "red"))
+ (khaki . ("#8A6C23" "color-94" "yellow"))
+ (lime . ("#358A2A" "color-28" "green"))
+ (magenta . ("#9A35B3" "color-127" "magenta"))
+ (pale-azure . ("#0A74B8" "color-31" "cyan"))
+ (red . ("#D22129" "color-160" "brightred"))
+ (salmon . ("#D1570B" "color-166" "brightyellow"))
+ (violet . ("#7A3B9E" "color-97" "magenta")))
+ "The color palette used throughout `mango-light-theme'.
+Each color is mapped to a list of colors of the form
+(GUI-HEX 256-COLOR 16-COLOR) for use in true-color, 256-color, and
+16-color modes.")
+
+(defsubst mango-light-theme-color (name &optional display)
+ "Get the color value of NAME for the given DISPLAY.
+DISPLAY can be 'gui, '256, or '16."
+ (let ((colors (alist-get name mango-light-theme-colors-alist)))
+ (pcase display
+ ('gui (nth 0 colors))
+ ('256 (nth 1 colors))
+ ('16 (nth 2 colors))
+ (_ (nth 0 colors)))))
+
+(defun mango-light-theme-spec (&rest props)
+ "Generate a tiered display specification list from PROPS.
+Values that match keys in `mango-light-theme-colors-alist' are
+automatically mapped to their correct display colors."
+ (let (gui c256 c16)
+ (while props
+ (let ((key (pop props))
+ (val (pop props)))
+ (push key gui)
+ (push key c256)
+ (push key c16)
+ (if (and (symbolp val) (alist-get val mango-light-theme-colors-alist))
+ (progn
+ (push (mango-light-theme-color val 'gui) gui)
+ (push (mango-light-theme-color val '256) c256)
+ (push (mango-light-theme-color val '16) c16))
+ (push val gui)
+ (push val c256)
+ (push val c16))))
+ `((((type graphic tty) (min-colors #x1000000))
+ ,(nreverse gui))
+ (((type tty) (min-colors 256))
+ ,(nreverse c256))
+ (((type tty))
+ ,(nreverse c16)))))
+
+(custom-theme-set-faces
+ 'mango-light
+
+ ;; Standard Stuff
+ `(default
+ ,(mango-light-theme-spec
+ :foreground 'foreground
+ :background 'background))
+ `(fringe
+ ((t (:inherit default))))
+
+ ;; Lines
+ `(hl-line
+ ,(mango-light-theme-spec
+ :background 'background-faint))
+ `(region
+ ,(mango-light-theme-spec
+ :background 'middleground))
+ `(header-line
+ ,(mango-light-theme-spec
+ :background 'middleground))
+ `(mode-line-active
+ ((t ( :box ,(mango-light-theme-color 'foreground 'gui)
+ :inherit header-line))))
+ `(mode-line-inactive
+ ,(mango-light-theme-spec
+ :background 'background-cool
+ :weight 'light))
+ `(window-divider
+ ,(mango-light-theme-spec
+ :foreground 'background-cool))
+ `(window-divider-first-pixel
+ ,(mango-light-theme-spec
+ :foreground 'background-cool))
+ `(window-divider-last-pixel
+ ,(mango-light-theme-spec
+ :foreground 'background-cool))
+
+ ;; Line Numbers
+ `(line-number
+ ,(mango-light-theme-spec
+ :foreground 'background-faint
+ :background 'background))
+ `(line-number-current-line
+ ,(mango-light-theme-spec
+ :foreground 'salmon
+ :background 'background
+ :weight 'bold))
+
+ ;; Documentation
+ `(font-lock-comment-face
+ ,(mango-light-theme-spec
+ :foreground 'khaki
+ :weight 'semi-bold))
+ `(font-lock-doc-face
+ ,(mango-light-theme-spec
+ :foreground 'disabled))
+
+ ;; Modeline
+ `(mm-modeline-overwrite-face
+ ((t (:weight bold))))
+ `(mm-modeline-readonly-face
+ ((t (:weight bold))))
+ `(mm-modeline-buffer-name-face
+ ((t (:inherit font-lock-constant-face))))
+ `(mm-modeline-buffer-modified-face
+ ((t (:inherit shadow))))
+ `(mm-modeline-major-mode-name-face
+ ((t (:weight bold))))
+ `(mm-modeline-major-mode-symbol-face
+ ((t (:inherit shadow))))
+ `(mm-modeline-git-branch-face
+ ((t (:inherit font-lock-constant-face))))
+ `(mm-modeline-narrow-face
+ ,(mango-light-theme-spec
+ :background 'dark-red
+ :box 'dark-red
+ :weight 'bold))
+
+ ;; Core Language
+ `(font-lock-builtin-face
+ ((t (:inherit font-lock-preprocessor-face))))
+ `(font-lock-keyword-face
+ ,(mango-light-theme-spec
+ :foreground 'violet))
+ `(font-lock-type-face
+ ,(mango-light-theme-spec
+ :foreground 'celestial-blue))
+
+ ;; Function-likes
+ `(font-lock-function-name-face
+ ,(mango-light-theme-spec
+ :foreground 'khaki))
+ `(font-lock-preprocessor-face
+ ,(mango-light-theme-spec
+ :foreground 'magenta
+ :weight 'bold))
+
+ ;; Variables
+ `(font-lock-constant-face
+ ((t ( :inherit font-lock-variable-name-face
+ :weight bold))))
+ `(font-lock-variable-name-face
+ ,(mango-light-theme-spec
+ :foreground 'pale-azure))
+
+ ;; Other literals
+ `(help-key-binding
+ ((t (:inherit font-lock-constant-face))))
+ `(font-lock-number-face
+ ,(mango-light-theme-spec
+ :foreground 'salmon))
+
+ ;; Org Mode
+ `(org-quote
+ ((t ( :inherit org-block
+ :slant italic))))
+ `(org-code
+ ,(mango-light-theme-spec
+ :foreground 'salmon))
+ `(org-verbatim
+ ,(mango-light-theme-spec
+ :foreground 'lime))
+ `(org-block
+ ,(mango-light-theme-spec
+ :background 'background-cool))
+ `(org-hide
+ ,(mango-light-theme-spec
+ :foreground 'background))
+
+ ;; Info Page
+ `(Info-quoted
+ ((t (:inherit default))))
+
+ ;; Magit
+ `(magit-diff-context-highlight
+ ((t (:inherit hl-line))))
+ `(magit-section-highlight
+ ((t (:inherit hl-line))))
+ `(magit-diff-hunk-heading
+ ,(mango-light-theme-spec
+ :background 'background-cool))
+ `(magit-diff-hunk-heading-highlight
+ ,(mango-light-theme-spec
+ :background 'middleground))
+ `(git-commit-summary
+ ,(mango-light-theme-spec
+ :foreground 'khaki))
+ `(git-commit-overlong-summary
+ ,(mango-light-theme-spec
+ :foreground 'foreground
+ :background 'red
+ :weight 'bold))
+
+ ;; Vertico
+ `(vertico-current ((t (:inherit hl-line))))
+
+ ;; Marginalia
+ `(mm-diffstat-counter-added
+ ((t ( :foreground "green"
+ :weight bold))))
+ `(mm-diffstat-counter-removed
+ ((t ( :foreground "red"
+ :weight bold))))
+ `(marginalia-documentation
+ ,(mango-light-theme-spec
+ :foreground 'disabled
+ :underline nil))
+
+ ;; Tempel
+ `(tempel-default
+ ,(mango-light-theme-spec
+ :slant 'italic
+ :background 'middleground))
+ `(tempel-field
+ ,(mango-light-theme-spec
+ :slant 'italic
+ :background 'middleground))
+ `(tempel-form
+ ,(mango-light-theme-spec
+ :slant 'italic
+ :background 'middleground)))
+
+(provide-theme 'mango-light)