Emacs.org
Welcome back to the church, my child.
Introduction
Living in Emacs
One of the often touted benefits of using Emacs and also the content of an overplayed joke, that I am obligated to repeat here to appease RMS, is that Emacs is a great operating system only lacking a decent text-editor. What this means in practice is that once you learn the way of Emacs, you tend to want to do everything in it, and you certainly can. Emacs can be your mail reader, music player, file browser and even window manager. (And since X is becoming legacy technology, someone has looked into making Emacs a wayland compositor.)
This is why for me the configuration of Emacs takes precedence over other programs. If you are doing all your grep-ing, awk-ing and sed-ing in bash or zsh, it makes sense to configure your shell quite a bit to suit your tastes and habits. For me, the more I use Emacs, the less time I spent in a terminal, so why spend a lot of time (and thought) to customise the shell.
There's another joke which I would like to repeat here:
I'm using Linux. A library that Emacs uses to communicate with Intel hardware. — Erwin, #emacs, Freenode.
I am sure even Saints in the Church of Emacs use the shell here and there (likely in one of Emacs' many terminal modes) and will have some quality of life improvements for it. You will find mine under Shell.
General settings
What follows here is my configuration of Emacs which mainly which mainly consists of snippets found from all over the internet and packages and their behaviour.
The early-init.el File
Some variables are best set early on during Emacs' initialisation. This can help inhibit loading and then unloading certain UI-elements for instance, which can be jarring (and takes unnecessary time).
;; C-*- lexical-binding: t; -*- (defun prot-emacs-add-to-list (list element) "Add to symbol of LIST the given ELEMENT. Simplified version of `add-to-list'." (set list (cons element (symbol-value list)))) (mapc (lambda (var) (prot-emacs-add-to-list var '(width . (text-pixels . 1200))) (prot-emacs-add-to-list var '(height . (text-pixels . 900))) (prot-emacs-add-to-list var '(scroll-bar-width . 10))) '(default-frame-alist initial-frame-alist)) (setq frame-resize-pixelwise t frame-inhibit-implied-resize t frame-title-format '("%b") ring-bell-function 'ignore use-dialog-box t ; only for mouse events, which I seldom use use-file-dialog nil use-short-answers t inhibit-splash-screen t inhibit-startup-screen t inhibit-x-resources t inhibit-startup-echo-area-message user-login-name ; read the docstring inhibit-startup-buffer-menu t) ;; I do not use those graphical elements by default, but I do enable ;; them from time-to-time for testing purposes or to demonstrate ;; something. NEVER tell a beginner to disable any of these. They ;; are helpful. (menu-bar-mode -1) (scroll-bar-mode -1) (tool-bar-mode -1) ;; Temporarily increase the garbage collection threshold. These ;; changes help shave off about half a second of startup time. The ;; `most-positive-fixnum' is DANGEROUS AS A PERMANENT VALUE. See the ;; `emacs-startup-hook' a few lines below for what I actually use. (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.5) ;; Same idea as above for the `file-name-handler-alist' and the ;; `vc-handled-backends' with regard to startup speed optimisation. ;; Here I am storing the default value with the intent of restoring it ;; via the `emacs-startup-hook'. (defvar prot-emacs--file-name-handler-alist file-name-handler-alist) (defvar prot-emacs--vc-handled-backends vc-handled-backends) (setq file-name-handler-alist nil vc-handled-backends nil) (add-hook 'emacs-startup-hook (lambda () (setq gc-cons-threshold (* 100 100 8) gc-cons-percentage 0.1 file-name-handler-alist prot-emacs--file-name-handler-alist vc-handled-backends prot-emacs--vc-handled-backends))) ;; Initialise installed packages at this early stage, by using the ;; available cache. I had tried a setup with this set to nil in the ;; early-init.el, but (i) it ended up being slower and (ii) various ;; package commands, like `describe-package', did not have an index of ;; packages to work with, requiring a `package-refresh-contents'. (setq package-enable-at-startup t) ;; Set custom file location early (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
Packages
;; -*- lexical-binding: t; -*- (setq package-vc-register-as-project nil) ; Emacs 30 (setq package-archives '(("gnu-elpa" . "https://elpa.gnu.org/packages/") ("nongnu" . "https://elpa.nongnu.org/nongnu/") ("melpa" . "https://melpa.org/packages/"))) ;; Highest number gets priority (what is not mentioned has priority 0) (setq package-archive-priorities '(("gnu-elpa" . 3) ("melpa" . 2) ("nongnu" . 1))) (setq package-install-upgrade-built-in t) (require 'use-package-ensure) (setq use-package-always-ensure t) (setq use-package-always-defer nil) (setq use-package-verbose t) ;; Load custom file (when (file-exists-p custom-file) (load custom-file))
Benchmark-init
(use-package benchmark-init :ensure t :hook ;; To disable collection of benchmark data after init is done. (after-init . benchmark-init/deactivate)) (defun xcvh/display-startup-info () "Display package count and startup time in the echo area." (message "%d packages loaded in %.2f seconds" (length package-activated-list) (float-time (time-subtract after-init-time before-init-time)))) (add-hook 'emacs-startup-hook #'xcvh/display-startup-info)
Helper functions
(defun xcvh/goto-line-with-feedback () "Show line numbers temporarily while using goto-line." (interactive) (unwind-protect (progn (display-line-numbers-mode 1) (call-interactively #'goto-line)) (display-line-numbers-mode -1))) (global-set-key (kbd "M-g M-g") 'xcvh/goto-line-with-feedback)
macOS
A macro to run code only on macOS
(defmacro with-mac (&rest body) "Evaluate BODY only if running on macOS." `(when (eq system-type 'darwin) ,@body)) (with-mac (setq default-frame-alist (append '((ns-transparent-titlebar . t) (ns-appearance . light)) ;; or light default-frame-alist)))
Adding homebrew to exec-path
To enable emacs to invoke binaries installed by homebrew we need to add /opt/homebrew/bin to exec-path.
(with-mac (add-to-list 'exec-path "/opt/homebrew/bin/brew")) (use-package exec-path-from-shell :ensure t :config (exec-path-from-shell-initialize))
Enable finding of gcc path for emacs-plus
(defun homebrew-gcc-paths () "Return GCC library paths from Homebrew installations. Detects paths for gcc and libgccjit packages to be used in LIBRARY_PATH." (let* ((paths '()) (brew-bin (or (executable-find "brew") (let ((arm-path "/opt/homebrew/bin/brew") (intel-path "/usr/local/bin/brew")) (cond ((file-exists-p arm-path) arm-path) ((file-exists-p intel-path) intel-path)))))) (when brew-bin ;; Get gcc paths. (let* ((gcc-prefix (string-trim (shell-command-to-string (concat brew-bin " --prefix gcc")))) (gcc-lib-current (expand-file-name "lib/gcc/current" gcc-prefix))) (push gcc-lib-current paths) ;; Find apple-darwin directory. (let* ((default-directory gcc-lib-current) (arch-dirs (file-expand-wildcards "gcc/*-apple-darwin*/*[0-9]"))) (when arch-dirs (push (expand-file-name (car (sort arch-dirs #'string>))) paths)))) ;; Get libgccjit paths (let* ((jit-prefix (string-trim (shell-command-to-string (concat brew-bin " --prefix libgccjit")))) (jit-lib-current (expand-file-name "lib/gcc/current" jit-prefix))) (push jit-lib-current paths))) (nreverse paths))) (defun setup-macos-native-comp-library-paths () "Set up LIBRARY_PATH for native compilation on macOS. Includes Homebrew GCC paths and CommandLineTools SDK libraries." (let* ((existing-paths (split-string (or (getenv "LIBRARY_PATH") "") ":" t)) (gcc-paths (homebrew-gcc-paths)) (clt-paths '("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib")) (unique-paths (delete-dups (append existing-paths gcc-paths clt-paths)))) (setenv "LIBRARY_PATH" (mapconcat #'identity unique-paths ":")))) ;; Set up library paths for native compilation on macOS. (with-mac (setup-macos-native-comp-library-paths)) ;;(with-mac ;;(add-to-list 'default-frame-alist '(undecorated-round . t)))
Use GNU coreutils' ls for dired
This depends on installing coreutils from homebrew as macOS' BSD version of ls does not support the --dired flag.
brew install coreutils
(with-mac (let ((gls (executable-find "gls"))) (when gls (setq insert-directory-program gls dired-use-ls-dired t dired-listing-switches "-aBhl --group-directories-first"))))
Run macOS Shortcuts interactively
(with-mac (defvar xcvh/ignored-shortcuts '("{dumb.*" "Cowbells" "session_.*" "New.*") "List of shortcut names or regular expressions to exclude.")) (with-mac (defun xcvh/get-macos-shortcuts () "Retrieve a list of available macOS Shortcuts. Excludes `xcvh/ignored-shortcuts'." (let ((all (split-string (shell-command-to-string "shortcuts list") "\n" t))) (cl-remove-if (lambda (shortcut) (seq-some (lambda (pattern) (string-match-p pattern shortcut)) xcvh/ignored-shortcuts)) all)))) (with-mac (defun xcvh/run-macos-shortcut (&optional shortcut) "Run a macOS Shortcut by name, or prompt interactively if not provided." (interactive (list (completing-read "Run shortcut: " (xcvh/get-macos-shortcuts)))) (shell-command (format "shortcuts run %s" (shell-quote-argument shortcut)))))
Emacs Server
(use-package server :ensure nil) (unless (server-running-p) (server-start))
Load other elisp
(use-package gtd :load-path "~/.emacs.d/lisp")
Embark
(use-package embark :bind (("C-." . embark-act) :map embark-email-map ("n" . #'xcvh/notmuch-search-from-sender))) (use-package embark-consult)
Aesthetics
Aesthetics are a huge part of computing for many people. If that wasn't the case, we would not have literally thousands of themes, colour schemes, monospaced fonts and opinions on how these affect our productivity.
Padding
(use-package spacious-padding) ;; These are the default values, but I keep them here for visibility. (setq spacious-padding-widths '( :internal-border-width 15 :header-line-width 6 :mode-line-width 1 :tab-width 4 :right-divider-width 30 :scroll-bar-width 8 :fringe-width 8)) ;; Read the doc string of `spacious-padding-subtle-mode-line' as it ;; is very flexible and provides several examples. (setq spacious-padding-subtle-frame-lines `( :mode-line-active 'default :mode-line-inactive vertical-border)) (spacious-padding-mode 1) ;; Set a key binding if you need to toggle spacious padding. (define-key global-map (kbd "<f8>") #'spacious-padding-mode)
Fonts
(with-mac
(set-fontset-font t 'unicode "Apple Symblos" nil 'prepend))
Fontaine presets for PragmataPro with and without ligatures and IBM Plex
(use-package fontaine :config (setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld")) (setq fontaine-presets '((pragmata-small :default-family "PragmataPro Mono Liga" :default-height 120 :variable-pitch-family "PragmataPro Liga") (noliga-small :inherit pragmata-small :default-family "PragmataPro Mono" :variable-pitch-family "PragmataPro") (plex-small :inherit pragmata-small :default-family "IBM Plex Mono" :variable-pitch-family "IBM Plex Sans") (berkeley-small :inherit pragmata-small :default-family "Berkeley Mono Variable" :variable-pitch-family "Berkeley Mono Variable") (pragmata-regular) (noliga-regular :inherit pragmata-regular :default-family "PragmataPro Mono" :variable-pitch-family "PragmataPro") (plex-regular :inherit pragmata-regular :default-family "IBM Plex Mono" :variable-pitch-family "IBM Plex Sans") (berkeley-regular :inherit pragmata-regular :default-family "Berkeley Mono Variable" :variable-pitch-family "Berkeley Mono Variable") (pragmata-medium :default-weight semilight :default-height 155 :bold-weight extrabold) (noliga-medium :inherit pragmata-medium :default-family "PragmataPro Mono" :variable-pitch-family "PragmataPro") (plex-medium :inherit pragmata-medium :default-family "IBM Plex Mono" :variable-pitch-family "IBM Plex Sans") (berkeley-medium :inherit pragmata-medium :default-family "Berkeley Mono Variable" :variable-pitch-family "Berkeley Mono Variable") (pragmata-large :inherit pragmata-medium :default-height 190) (noliga-large :inherit noliga-medium :default-height 190) (plex-large :inherit plex-medium :default-height 190) (berkeley-large :inherit berkeley-medium :default-height 190) (pragmata-presentation :default-height 220) (noliga-presentation :inherit pragmata-presentation :default-family "PragmataPro Mono" :variable-pitch-family "PragmataPro") (plex-presentation :inherit pragmata-presentation :default-family "IBM Plex Mono" :variable-pitch-family "IBM Plex Sans") (berkeley-presentation :inherit pragmata-presentation :default-family "Berkeley Mono Variable" :variable-pitch-family "Berkeley Mono Variable") (t :default-family "PragmataPro Mono Liga" :default-weight regular :default-height 140 :variable-pitch-family "PragmataPro Liga" :fixed-pitch-family nil :fixed-pitch-weight nil :fixed-pitch-height 1.0 :fixed-pitch-serif-family nil :fixed-pitch-serif-weight nil :fixed-pitch-serif-height 1.0 :variable-pitch-weight nil :variable-pitch-height 1.0 :mode-line-active-family nil :mode-line-active-weight nil :mode-line-active-height 0.9 :mode-line-inactive-family nil :mode-line-inactive-weight nil :mode-line-inactive-height 0.9 :header-line-family nil :header-line-weight nil :header-line-height 0.9 :line-number-family nil :line-number-weight nil :line-number-height 0.9 :tab-bar-family nil :tab-bar-weight nil :tab-bar-height 1.0 :tab-line-family nil :tab-line-weight nil :tab-line-height 1.0 :bold-family nil :bold-weight bold :italic-family nil :italic-slant italic :line-spacing nil) (pragmata-t :inherit t) (noliga-t :inherit t :default-family "PragmataPro Mono" :variable-pitch-family "PragmataPro") (plex-t :inherit t :default-family "IBM Plex Mono" :variable-pitch-family "IBM Plex Sans") (berkeley-t :inherit t :default-family "Berkeley Mono Variable" :variable-pitch-family "Berkeley Mono Variable")))) (fontaine-set-preset (or (fontaine-restore-latest-preset) 'berkeley-small)) (fontaine-mode 1)
Ligature support for PragmataPro
(use-package pragmatapro-lig :vc (:url "https://github.com/lumiknit/emacs-pragmatapro-ligatures" :rev :newest) ;; :hook ((prog-mode text-mode) . pragmatapro-lig-mode) ) (defun xcvh/fontaine-set-preset (name) "Set fontaine preset NAME and toggle `pragmatapro-lig-mode'." (interactive (list (intern (completing-read "Preset: " (mapcar #'car fontaine-presets))))) (fontaine-set-preset name) (cond ;; liga variants ((string-prefix-p "noliga-" (symbol-name name)) (when (bound-and-true-p pragmatapro-lig-mode) (pragmatapro-lig-mode -1))) ((string-prefix-p "plex-" (symbol-name name)) (when (bound-and-true-p pragmatapro-lig-mode) (pragmatapro-lig-mode -1))) ((string-prefix-p "berkeley-" (symbol-name name)) (when (bound-and-true-p pragmatapro-lig-mode) (pragmatapro-lig-mode -1))) (t (unless (bound-and-true-p pragmatapro-lig-mode) (pragmatapro-lig-mode 1))))) ;; Optional: bind instead of calling fontaine directly (global-set-key (kbd "C-c f") #'xcvh/fontaine-set-preset)
Themes
Leuven
(mapc #'disable-theme custom-enabled-themes) (use-package leuven-theme :config (load-theme 'leuven t))
Toggle theme
(defun xcvh/toggle-themes () "Toggle between light and dark themes." (interactive) (let ((is-light (member 'leuven custom-enabled-themes))) (mapc #'disable-theme custom-enabled-themes) (if is-light (load-theme 'leuven-dark t) (load-theme 'leuven t)))) (global-set-key (kbd "C-c t t") #'xcvh/toggle-themes)
Modeline
mood-line
(use-package mood-line :config (mood-line-mode) ;; First, reset to see default again (setq mood-line-format mood-line-format-default) :custom (mood-line-glyph-alist mood-line-glyphs-fira-code)) ;; Add word/char count to cursor position segment (defun my/mood-line-wc () (format " W%d:C%d" (count-words (point-min) (point-max)) (- (point-max) (point-min)))) (advice-add 'mood-line-segment-cursor-position :filter-return (lambda (pos) (concat pos (my/mood-line-wc))))
Transmission
(use-package transmission)
Music
(use-package listen :custom (listen-directory . ("~/Music/Slsk/")))
Completion and the Minibuffer
Reminder! By default Emacs comes with the ability to dwim (do what i mean).
In relation to find-file this might be expanding Find file: ~/w/s/j/j/c/i/s with tab to Find file: ~/workspace/sr.ht/jeldo/jel.do/content/images/saint-rms.png as long as that path is unique.
(use-package vertico :config (vertico-mode 1)) (use-package marginalia :config (marginalia-mode 1)) (use-package consult :bind (("C-s" . consult-line) ("C-c t s" . consult-theme) ("C-x b" . consult-buffer) ("M-g i" . consult-imenu) ("C-c r" . consult-ripgrep))) (use-package orderless :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) (use-package corfu :init (global-corfu-mode) :custom (corfu-auto t) ;; Enable auto-completion (corfu-cycle t) ;; Allows cycling through candidates (corfu-preselect 'prompt) ;; Don't preselect first candidate (corfu-quit-no-match 'separator) ;; Quit if no match (corfu-auto-delay 0.2) (corfu-auto-prefix 2)) (use-package cape :init (add-to-list 'completion-at-point-functions #'cape-dabbrev) (add-to-list 'completion-at-point-functions #'cape-file))
Dired and File Management
(require 'dired) (require 'dired-x) (require 'wdired) (require 'hl-line) (require 'mouse) (require 'image-dired) (require 'image-dired-dired) (require 'casual-dired) (keymap-set dired-mode-map "C-o" #'casual-dired-tmenu) (keymap-set dired-mode-map "s" #'casual-dired-sort-by-tmenu) (keymap-set dired-mode-map "/" #'casual-dired-search-replace-tmenu) (keymap-set dired-mode-map "s-<return>" #'dired-do-open) (add-hook 'dired-mode-hook 'hl-line-mode) (add-hook 'dired-mode-hook 'context-menu-mode) (add-hook 'dired-mode-hook 'dired-async-mode) ;; (add-hook 'dired-mode-hook 'dired-hide-details-mode) (add-hook 'dired-mode-hook (lambda () (setq-local mouse-1-click-follows-link 'double))) (setq delete-by-moving-to-trash t) (keymap-set dired-mode-map "M-o" #'dired-omit-mode) (keymap-set dired-mode-map "E" #'wdired-change-to-wdired-mode) (keymap-set dired-mode-map "M-n" #'dired-next-dirline) (keymap-set dired-mode-map "M-p" #'dired-prev-dirline) (keymap-set dired-mode-map "]" #'dired-next-subdir) (keymap-set dired-mode-map "[" #'dired-prev-subdir) (keymap-set dired-mode-map "A-M-<mouse-1>" #'browse-url-of-dired-file) (keymap-set dired-mode-map "<backtab>" #'dired-prev-subdir) (keymap-set dired-mode-map "TAB" #'dired-next-subdir) (keymap-set dired-mode-map "M-j" #'dired-goto-subdir) (keymap-set dired-mode-map ";" #'image-dired-dired-toggle-marked-thumbs) (keymap-set image-dired-thumbnail-mode-map "n" #'image-dired-display-next) (keymap-set image-dired-thumbnail-mode-map "p" #'image-dired-display-previous) (use-package nerd-icons-dired :ensure t :hook (dired-mode . nerd-icons-dired-mode))
Git and Version Control
Git really needs no introduction. It is one of the most foundational technologies in distributed software development. But even if you're not a software developer and will use Emacs only for organising your life in Org Mode or to configure a more text based computing evironment—everytime you incrementally work on text, it can be argued that a version control system should be in place. And for most people that's git. Emacs comes with version control capabilities out of the box and it can support a couple of different backends. See Emacs Version Control.
A very popular (and powerful) package for Emacs that has excellent ergonomics is Magit.
(use-package magit :custom (magit-diff-refine-hunk 'all) ; Highlight the actual change :hook (magit-mode . visual-line-mode) :bind ("C-x g" . magit-status))
Passwords and Bookmarks
(use-package ebuku) (use-package pass)
Notmuch
(use-package notmuch :load-path "/opt/homebrew/share/emacs/site-lisp/notmuch/" :ensure nil :demand t :bind ((:map notmuch-search-mode-map ("F" . xcvh/notmuch-filter-by-from) ("f" . xcvh/notmuch-search-by-from) ("A" . xcvh/notmuch-archive-all-visible) ("D" . xcvh/notmuch-delete-all-visible)) (:map notmuch-show-mode-map ("A" . notmuch-show-archive-thread-then-next))) :config (setq notmuch-archive-tags '("-inbox") mm-text-html-renderer 'shr shr-width 80 shr-use-colors nil notmuch-multipart/alternative-discouraged '("text/plain" "text/html") notmuch-search-oldest-first nil) (defun xcvh/notmuch-filter-by-from () "Filter this view by messages from the sender of the current thread." (interactive) (let* ((authors (notmuch-search-find-authors)) (from (car (split-string authors "[,<]" t " ")))) (notmuch-search-filter (format "from:%s" from)))) (defun xcvh/notmuch-search-by-from () "Search all messages from the sender of the current thread." (interactive) (let* ((authors (notmuch-search-find-authors)) (from (car (split-string authors "[,<]" t " ")))) (notmuch-search (format "from:%s" from)))) (defun xcvh/notmuch-archive-all-visible () "Archive all visible messages." (interactive) (notmuch-search-tag-all '("-inbox"))) (defun xcvh/notmuch-delete-all-visible () "Delete all visible messages after confirmation." (interactive) (when (yes-or-no-p "Delete all visible messages? ") (notmuch-search-tag-all '("+deleted")))) (defun xcvh/notmuch-search-from-sender (email) "Search notmuch for emails from EMAIL." (notmuch-search (concat "from:" email))))
Indicator
(use-package notmuch-indicator :config (setq notmuch-indicator-args '((:terms "tag:unread and tag:inbox" :label "@") (:terms "tag:todo" :label "!")) notmuch-indicator-add-to-mode-line-misc-info t notmuch-indicator-refresh-count (* 60 3) notmuch-indicator-hide-empty-counters t notmuch-indicator-force-refresh-commands '(notmuch-refresh-this-buffer)) ;; Add clickable button to mode line (add-to-list 'global-mode-string (list " " ; separator (propertize "~hello" 'mouse-face 'mode-line-highlight 'help-echo "Click to open notmuch" 'local-map (let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'notmuch-hello) map))) t) (notmuch-indicator-mode 1))
Reading
Ebooks
justified nov
(use-package nov :mode ("\\.epub\\'" . nov-mode) :hook (nov-mode . xcvh/nov-font-setup) :config (defun xcvh/nov-font-setup () (face-remap-add-relative 'variable-pitch :family "Brill" :height 1.2) (setq-local shr-width nil shr-max-width nil)) (setq nov-text-width t) (defun my-nov-window-configuration-change-hook () (my-nov-post-html-render-hook) (remove-hook 'window-configuration-change-hook 'my-nov-window-configuration-change-hook t)) (defun my-nov-post-html-render-hook () (if (get-buffer-window) (let ((max-width (pj-line-width)) buffer-read-only) (save-excursion (goto-char (point-min)) (while (not (eobp)) (when (not (looking-at "^[[:space:]]*$")) (goto-char (line-end-position)) (when (> (shr-pixel-column) max-width) (goto-char (line-beginning-position)) (pj-justify))) (forward-line 1)))) (add-hook 'window-configuration-change-hook 'my-nov-window-configuration-change-hook nil t))) (add-hook 'nov-post-html-render-hook 'my-nov-post-html-render-hook)) (require 'justify-kp)
Feeds and Bookmarks
(use-package pinboard :config (add-to-list 'auth-sources "~/.authinfo" t)) (use-package elfeed :config (setq elfeed-feeds '("https://velvetshark.com/articles" "https://joshblais.com/posts/index.xml" "https://protesilaos.com/news.xml" "https://protesilaos.com/codelog.xml" "https://200ok.ch/atom.xml" "https://sqrtminusone.xyz/posts/")))
Search for word at point
(defvar xcvh/search-map (make-sparse-keymap) "Keymap for personal search and lookup functions.") (bind-key "C-c s" xcvh/search-map) (use-package define-word :bind (:map xcvh/search-map ("d" . define-word-at-point))) (defun xcvh/duckduckgo-search (text) "Search DuckDuckGo from Emacs." (interactive "sSearch: ") (browse-url (concat "https://duckduckgo.com/?q=" (replace-regexp-in-string " " "+" text)))) (defun xcvh/duckduckgo-search-word-at-point () "Search DuckDuckGo for word at point from Emacs. When the region is active, define the marked phrase." (interactive) (let ((word (cond ((eq major-mode 'pdf-view-mode) (car (pdf-view-active-region-text))) ((use-region-p) (buffer-substring-no-properties (region-beginning) (region-end))) (t (substring-no-properties (thing-at-point 'word)))))) (xcvh/duckduckgo-search word))) (bind-key "s" 'xcvh/duckduckgo-search-word-at-point xcvh/search-map)
Programming
Undo-tree
(use-package undo-tree :ensure t :config (setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/undo")) undo-tree-auto-save-history t undo-tree-visualizer-diff t undo-tree-visualizer-timestamps t) (global-undo-tree-mode))
(use-package expand-region :bind ("C-=" . er/expand-region)) ;; Configure Tempel (use-package tempel ;; Require trigger prefix before template name when completing. ;; :custom ;; (tempel-trigger-prefix "<") :bind (("M-+" . tempel-complete) ;; Alternative tempel-expand ("M-*" . tempel-insert)) :init ;; Setup completion at point (defun tempel-setup-capf () ;; Add the Tempel Capf to `completion-at-point-functions'. ;; `tempel-expand' only triggers on exact matches. Alternatively use ;; `tempel-complete' if you want to see all matches, but then you ;; should also configure `tempel-trigger-prefix', such that Tempel ;; does not trigger too often when you don't expect it. NOTE: We add ;; `tempel-expand' *before* the main programming mode Capf, such ;; that it will be tried first. (setq-local completion-at-point-functions (cons #'tempel-expand completion-at-point-functions))) (add-hook 'conf-mode-hook 'tempel-setup-capf) (add-hook 'prog-mode-hook 'tempel-setup-capf) (add-hook 'text-mode-hook 'tempel-setup-capf) ;; Optionally make the Tempel templates available to Abbrev, ;; either locally or globally. `expand-abbrev' is bound to C-x '. ;; (add-hook 'prog-mode-hook #'tempel-abbrev-mode) ;; (global-tempel-abbrev-mode) ) ;; Optional: Add tempel-collection. ;; The package is young and doesn't have comprehensive coverage. (use-package tempel-collection) (use-package hungry-delete :ensure t :bind (("s-<backspace>" . hungry-delete-backward) ("s-<delete>" . hungry-delete-forward)) :config (global-hungry-delete-mode)) (use-package diff-hl :ensure t :hook ((prog-mode . diff-hl-margin-mode) (org-mode . diff-hl-margin-mode) (dired-mode . diff-hl-dired-mode) (magit-post-refresh . diff-hl-magit-post-refresh)) :config (setq diff-hl-margin-side 'left diff-hl-fringe-bmp-function 'diff-hl-fringe-bmp-from-type) (global-diff-hl-mode 1) (diff-hl-flydiff-mode 1))
Projects
(use-package project :ensure nil ; built-in package :bind (("C-x p f" . project-find-file) ("C-x p p" . project-switch-project) ("C-x p d" . project-dired) ("C-x p g" . project-find-regexp) ("C-x p r" . project-query-replace-regexp) ("C-x p b" . consult-project-buffer)) :config ;; Auto-discover projects under specific directories (project-remember-projects-under "~/Developer/" t)) (use-package grep :ensure t :config (grep-apply-setting 'grep-find-command '("rg -n -H --no-heading -e '' $(git rev-parse --show-toplevel || pwd)" . 27) ) (global-set-key (kbd "C-x C-g") 'grep-find))
Exercism
(use-package exercism :ensure t :bind ("C-c e" . exercism))
Lisps
Elisp
Clojure
Paredit reindents the current and surrounding lines when you insert delimiters, and that reindentation was using semantic rules despite your clojure-ts-indent-style being set to 'fixed. The fix works because by setting lisp-indent-function to clojure-ts-indent-line, you're ensuring paredit's delimiter-triggered reindentation uses the correct function that respects your indent style setting.
(use-package clojure-ts-mode) (setopt clojure-ts-indent-style 'fixed) (add-hook 'clojure-ts-mode-hook (lambda () (setq-local lisp-indent-function #'clojure-ts-indent-line))) (use-package cider)
Paredit
(use-package paredit :ensure t :hook ((clojure-mode clojure-ts-mode emacs-lisp-mode) . paredit-mode) :bind ("M-w" . paredit-copy-as-kill) ("{" . paredit-open-curly) ("}" . paredit-close-curly) :config ;; (defun my/paredit-meow-bindings () ;; (when (bound-and-true-p meow-normal-mode) ;; (meow-normal-define-key ;; '(")" . paredit-forward-slurp-sexp) ;; '("(" . paredit-forward-barf-sexp)))) ;; (add-hook 'paredit-mode-hook #'my/paredit-meow-bindings) )
envrc.el
(use-package envrc :hook (after-init . envrc-global-mode))
AppleScript
(use-package applescript-mode :ensure t :mode ("\\.\\(applescript\\|scpt\\)\\'" . applescript-mode) :hook (applescript-mode . font-lock-mode))
Prose and Documentation
TODO Typst cleanup
(add-to-list 'treesit-language-source-alist
'(typst "https://github.com/uben0/tree-sitter-typst"))
(add-to-list 'treesit-language-source-alist
'(xml "https://github.com/tree-sitter-grammars/tree-sitter-xml"))
(treesit-install-language-grammar 'typst)
(use-package typst-ts-mode
:ensure t
:custom
;; don't add "--open" if you'd like `watch` to be an error detector
(typst-ts-mode-watch-options "--open")
(typst-ts-preview-function #'find-file))
(with-eval-after-load 'eglot
(with-eval-after-load 'typst-ts-mode
(add-to-list 'eglot-server-programs
`((typst-ts-mode) .
,(eglot-alternatives `(,typst-ts-lsp-download-path
"tinymist"
"typst-lsp"))))))
(setq-default eglot-workspace-configuration
'(:tinymist (:exportPdf "onSave")))
(defun xcvh/typst-toggle-bold-region (start end)
"Toggle Typst bold syntax on selected text."
(interactive "r")
(if (use-region-p)
(let ((text (buffer-substring start end)))
(if (and (string-prefix-p "*" text)
(string-suffix-p "*" text)
(> (length text) 2))
;; Remove bold
(progn
(delete-region start end)
(insert (substring text 1 -1)))
;; Add bold
(delete-region start end)
(insert (format "*%s*" text))))
(insert "**")
(backward-char 1)))
(defun xcvh/typst-toggle-emph-region (start end)
"Toggle Typst emphasis syntax on selected text."
(interactive "r")
(if (use-region-p)
(let ((text (buffer-substring start end)))
(if (and (string-prefix-p "#emph[" text)
(string-suffix-p "]" text)
(> (length text) 7))
;; Remove emphasis
(progn
(delete-region start end)
(insert (substring text 6 -1)))
;; Add emphasis
(delete-region start end)
(insert (format "#emph[%s]" text))))
(insert "#emph[]")
(backward-char 1)))
(with-eval-after-load 'typst-ts-mode
(define-key typst-ts-mode-map (kbd "s-b") 'xcvh/typst-toggle-bold-region)
(define-key typst-ts-mode-map (kbd "s-i") 'xcvh/typst-toggle-emph-region))
(defun my-typst-compile-master-and-preview ()
"Compile and preview main.typ instead of current buffer."
(interactive)
(let ((master-file (or (and (boundp 'typst-ts-master-file)
typst-ts-master-file)
"main.typ"))
(original-buffer (current-buffer)))
(if (file-exists-p master-file)
(progn
(save-buffer)
(find-file master-file)
(typst-ts-compile-and-preview)
(switch-to-buffer original-buffer))
(message "Master file %s not found" master-file))))
(defun my-typst-compile-master-and-zathura ()
"Compile main.typ and open/update in Zathura."
(interactive)
(let ((master-file (or (and (boundp 'typst-ts-master-file)
typst-ts-master-file)
"main.typ"))
(pdf-file (or (and (boundp 'typst-ts-master-file)
(concat (file-name-sans-extension typst-ts-master-file) ".pdf"))
"main.pdf")))
(if (file-exists-p master-file)
(progn
(save-buffer)
;; Compile the master file
(shell-command (format "typst compile %s" (shell-quote-argument master-file)))
;; Check if Zathura is already running with this PDF
(let ((zathura-running
(string-match-p pdf-file
(shell-command-to-string "ps aux | grep zathura"))))
(unless zathura-running
;; Open in Zathura with --fork so it doesn't block
(start-process "zathura" nil "zathura" "--fork" pdf-file)))
(message "Compiled %s and updated Zathura" master-file))
(message "Master file %s not found" master-file))))
(with-eval-after-load 'typst-ts-mode
(define-key typst-ts-mode-map (kbd "C-c C-c") #'my-typst-compile-master-and-zathura))
(use-package pdf-tools
:ensure t
:mode ("\\.pdf\\'" . pdf-view-mode)
:config
(pdf-tools-install :no-query)
(setq-default pdf-view-display-size 'fit-page)
;; Optional: Enable auto-revert for PDFs
(add-hook 'pdf-view-mode-hook (lambda () (auto-revert-mode 1)))
;; Optional: More comfortable scrolling
(setq pdf-view-resize-factor 1.1)
;; Optional: Midnight mode (dark theme for PDFs)
;; (add-hook 'pdf-view-mode-hook (lambda () (pdf-view-midnight-minor-mode)))
:bind (:map pdf-view-mode-map
("C-s" . isearch-forward)
("C-r" . isearch-backward)
("j" . pdf-view-next-line-or-next-page)
("k" . pdf-view-previous-line-or-previous-page)))
(defun my-pdf-tools-backdrop ()
(face-remap-add-relative 'default '(:background "#f2f2f2")))
(add-hook 'pdf-tools-enabled-hook #'my-pdf-tools-backdrop)
(use-package jinx
:hook (emacs-startup . global-jinx-mode)
:bind (("M-$" . jinx-correct)
("C-M-$" . jinx-languages))
:config
(setq jinx-languages "en_GB-ise"))
;; ;; This is a wrong word. Customise is wrong.
;; ;; Limitations:
;; ;; 1. since raw block highlighting let the local parser highlights the area,
;; ;; some area doesn't contain face (like `bash-ts-mode'), and those areas will
;; ;; be checked by jinx
;; (add-to-list
;; 'jinx-exclude-faces
;; '(typst-ts-mode
;; ;; not included font lock faces
;; ;; `font-lock-comment-face', `font-lock-string-face', `font-lock-doc-face'
;; ;; `font-lock-doc-markup-face'
;; font-lock-warning-face font-lock-function-name-face font-lock-function-call-face
;; font-lock-variable-name-face font-lock-variable-use-face font-lock-keyword-face
;; font-lock-comment-delimiter-face font-lock-type-face font-lock-constant-face
;; font-lock-builtin-face font-lock-preprocessor-face
;; font-lock-negation-char-face font-lock-escape-face font-lock-number-face
;; font-lock-operator-face font-lock-property-use-face font-lock-punctuation-face
;; font-lock-bracket-face font-lock-delimiter-face font-lock-misc-punctuation-face
;; ;; typst-ts-mode created faces
;; typst-ts-markup-item-indicator-face typst-ts-markup-term-indicator-face
;; typst-ts-markup-rawspan-indicator-face typst-ts-markup-rawspan-blob-face
;; typst-ts-markup-rawblock-indicator-face typst-ts-markup-rawblock-lang-face
;; typst-ts-markup-rawblock-blob-face
;; typst-ts-error-face typst-ts-shorthand-face typst-ts-markup-linebreak-face
;; typst-ts-markup-quote-face typst-ts-markup-url-face typst-ts-math-indicator-face))
howm
(use-package howm :ensure t :init (require 'howm-org) (setq howm-directory "~/Documents/Howm/" howm-home-directory "~/Documents/Howm/" howm-keyword-file (expand-file-name ".howm-keys" howm-home-directory) howm-history-file (expand-file-name ".howm-history" howm-home-directory)) (setq howm-follow-theme t) ;; Use ripgrep ;; (setq howm-view-use-grep t) ;; (setq howm-view-grep-command "rg") ;; (setq howm-view-grep-option "-nH --no-heading --color never") ;; (setq howm-view-grep-extended-option nil) ;; (setq howm-view-grep-fixed-option "-F") ;; (setq howm-view-grep-expr-option nil) ;; (setq howm-view-grep-file-stdin-option nil) )
calfw
(use-package calfw) ;(use-package calfw-howm) (eval-after-load "howm-menu" '(progn (require 'calfw-howm) (calfw-howm-install-schedules) (define-key howm-mode-map (kbd "M-C") 'calfw-howm-open-calendar) ))
Proposals
(use-package whitespace :config (setq whitespace-style (remove 'lines (remove 'lines-tail whitespace-style)))) (use-package atomic-chrome :ensure t :demand t ; force immediate loading :hook (atomic-chrome-edit-mode . whitespace-mode) (atomic-chrome-edit-mode . highlight-placeholders-mode) :config (add-hook 'atomic-chrome-edit-mode-hook #'xcvh/clean-whitespace) (add-hook 'atomic-chrome-edit-mode-hook #'delete-other-windows) (atomic-chrome-start-server))
(defun xcvh/replace-bullets-with-dashes () "Query-replace common list bullets with em-dashes, preserving indentation." (interactive) (let ((start (if (use-region-p) (region-beginning) (point-min))) (end (if (use-region-p) (region-end) (point-max)))) (save-excursion (goto-char start) (query-replace-regexp "^\\([[:space:]]*\\)[*+•◦▪▸-–—][[:space:]]+" "\\1– " nil start end)))) (defun xcvh/clean-whitespace () "Remove unnecessary whitespace while preserving indentation." (interactive) (save-excursion ;; Remove three-or-four-digit/three-or-four-digit pattern from first line (goto-char (point-min)) (when (re-search-forward "[0-9]\\{3,4\\}/[0-9]\\{3,4\\}" (line-end-position) t) (replace-match "") ;; If line is now empty or whitespace-only, delete it (beginning-of-line) (when (looking-at "^[[:space:]]*$") (delete-region (point) (progn (forward-line 1) (point))))) ;; First pass: remove trailing whitespace and clean whitespace-only lines (goto-char (point-min)) (while (not (eobp)) (beginning-of-line) ;; If line has only whitespace, delete it all (when (looking-at "^[[:space:]]+$") (delete-region (point) (line-end-position))) ;; Remove trailing whitespace (end-of-line) (skip-chars-backward " \t") (delete-region (point) (line-end-position)) (forward-line 1)) ;; Convert tabs to spaces (untabify (point-min) (point-max)) ;; Collapse multiple consecutive blank lines to one (goto-char (point-min)) (while (re-search-forward "\\(\n\\)\n\\{2,\\}" nil t) (replace-match "\\1\n"))) (goto-char 0)) (global-set-key (kbd "C-c w") 'xcvh/clean-whitespace) (defface my-placeholder-face '((t :background "orange" :foreground "white" :weight bold)) "Face for placeholder words.") (define-minor-mode highlight-placeholders-mode "Highlight placeholder words like xxx, tbd, bla, Text box." :lighter " PH" (if highlight-placeholders-mode (font-lock-add-keywords nil '(("\\<\\(xxx\\|tbd\\|bla\\)\\>" 0 'my-placeholder-face prepend) ("Text box" 0 'my-placeholder-face prepend))) (font-lock-remove-keywords nil '(("\\<\\(xxx\\|tbd\\|bla\\)\\>" 0 'my-placeholder-face prepend) ("Text box" 0 'my-placeholder-face prepend)))) (font-lock-flush))
Auto
| typst-ts-mode | font-lock-warning-face | font-lock-function-name-face | font-lock-function-call-face | font-lock-variable-name-face | font-lock-variable-use-face | font-lock-keyword-face | font-lock-comment-delimiter-face | font-lock-type-face | font-lock-constant-face | font-lock-builtin-face | font-lock-preprocessor-face | font-lock-negation-char-face | font-lock-escape-face | font-lock-number-face | font-lock-operator-face | font-lock-property-use-face | font-lock-punctuation-face | font-lock-bracket-face | font-lock-delimiter-face | font-lock-misc-punctuation-face | typst-ts-markup-item-indicator-face | typst-ts-markup-term-indicator-face | typst-ts-markup-rawspan-indicator-face | typst-ts-markup-rawspan-blob-face | typst-ts-markup-rawblock-indicator-face | typst-ts-markup-rawblock-lang-face | typst-ts-markup-rawblock-blob-face | typst-ts-error-face | typst-ts-shorthand-face | typst-ts-markup-linebreak-face | typst-ts-markup-quote-face | typst-ts-markup-url-face | typst-ts-math-indicator-face |
| markdown-mode | markdown-code-face | markdown-html-attr-name-face | markdown-html-attr-value-face | markdown-html-tag-name-face | markdown-inline-code-face | markdown-link-face | markdown-markup-face | markdown-plain-url-face | markdown-reference-face | markdown-url-face | |||||||||||||||||||||||
| org-mode | org-block | org-block-begin-line | org-block-end-line | org-code | org-cite | org-cite-key | org-date | org-document-info-keyword | org-done | org-drawer | org-footnote | org-formula | org-latex-and-related | org-link | org-macro | org-meta-line | org-property-value | org-special-keyword | org-tag | org-todo | org-verbatim | org-warning | org-modern-tag | org-modern-date-active | org-modern-date-inactive | ||||||||
| tex-mode | tex-math | font-latex-math-face | font-latex-sedate-face | font-latex-verbatim-face | font-lock-function-name-face | font-lock-keyword-face | font-lock-variable-name-face | ||||||||||||||||||||||||||
| texinfo-mode | font-lock-function-name-face | font-lock-keyword-face | font-lock-variable-name-face | ||||||||||||||||||||||||||||||
| rst-mode | rst-literal | rst-external | rst-directive | rst-definition | rst-reference | ||||||||||||||||||||||||||||
| sgml-mode | font-lock-function-name-face | font-lock-variable-name-face | |||||||||||||||||||||||||||||||
| lisp-data-mode | font-lock-constant-face | font-lock-warning-face | |||||||||||||||||||||||||||||||
| message-mode | message-separator | message-header-cc | message-header-name | message-header-newsgroups | message-header-other | message-header-to | message-header-xheader | message-cited-text-1 | message-cited-text-2 | message-cited-text-3 | message-cited-text-4 | gnus-cite-1 | gnus-cite-2 | gnus-cite-3 | gnus-cite-4 | gnus-cite-5 | gnus-cite-6 | gnus-cite-7 | gnus-cite-8 | gnus-cite-9 | gnus-cite-10 | gnus-cite-11 |