diff options
| author | Thomas Voss <mail@thomasvoss.com> | 2026-04-03 02:10:14 +0200 |
|---|---|---|
| committer | Thomas Voss <mail@thomasvoss.com> | 2026-04-03 02:10:14 +0200 |
| commit | 8491e264f352447bd731f37edd1f0fe0b59c0194 (patch) | |
| tree | b3e075c046d6a389492fba89e3424107513462f8 | |
| parent | daeeecee613987bd821b4e15d03b2cb925cd88b0 (diff) | |
emacs: Introduce the new cleaned-up configuration
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 ?& "&" table) + (puthash ?< "<" table) + (puthash ?> ">" table) + (puthash ?\" """ table) + (puthash ?' "'" 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) |