GNU Emacs literate configuration
Introduction
This document holds my customizations for GNU Emacs. Its source
version is written in Org mode, utilizing Babel to realize literate
programming. The Elisp code blocks scattered throughout this
document are bundled together to create an Elisp library called
esy.el
, which Emacs executes on startup.
The source of this document is managed with Git in my dotfiles repository hosted on SourceHut. An online HTML version of this Emacs configuration is also published on my website.
Last modification time
This file was last updated at:
07-02-2023
Current source control revision
Git revision of this file:
58ef0679439d4a756235774c5357e97859469911
Fresh installation
To bootstrap this configuration, fetch a local clone of the repository from
SourceHut and create a symlink from the .emacs.d
subdirectory into your home
directory, possibly using GNU Stow.
$ git clone https://git.sr.ht/~eshel/dotfiles $ stow -t ~ dotfiles/.emacs.d $ emacs
After the first run of the provided init.el
, modifications to
esy.org
will be made available automatically whenever Emacs
restarts. See also Literate config bootstrap.
Elisp Header
For further information about Elisp headers, see elisp#Library Headers.
;;; esy.el --- GNU Emacs configuration -*- lexical-binding: t -*- ;; Copyright (C) 2021-2022 Eshel Yaron ;; Author: Eshel Yaron <[email protected]> ;; URL: https://eshelyaron.com/esy.html ;; This file is free software: you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by the ;; Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; This file is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this file. If not, see <http://www.gnu.org/licenses/>. ;;; Package-Requires: ((emacs "29")) ;;; Commentary: ;; Tangled version of esy.org ;;; Code:
Allow for more memory usage during initialization
(let ((normal-gc-cons-threshold (* 20 1024 1024)) (init-gc-cons-threshold (* 1024 1024 1024))) (setq gc-cons-threshold init-gc-cons-threshold) (add-hook 'emacs-startup-hook (lambda () (setq gc-cons-threshold normal-gc-cons-threshold))))
Suppressing compilation warnings
(setq native-comp-async-report-warnings-errors 'silent) (setq warning-minimum-level :error)
Package archives
(require 'package) (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/")) (add-to-list 'package-archives '("elpa-devel" . "https://elpa.gnu.org/devel/")) (setq package-archive-column-width 12 package-version-column-width 28)
Selected packages
These are external Emacs packages that I want to install:
(setq package-selected-packages '( all-the-icons all-the-icons-completion all-the-icons-dired all-the-icons-gnus auctex auctex-latexmk avy bbdb corfu define-word diff-hl ef-themes elfeed embark-consult gnu-elpa-keyring-update gnuplot graphviz-dot-mode graphql-mode haskell-mode htmlize ialign jenkinsfile-mode keycast kubernetes lin magit marginalia markdown-mode mastodon no-littering ob-prolog orderless org-modern package-lint paredit pdf-tools rainbow-delimiters rainbow-mode request rg slack smtpmail-multi sqlformat typit terraform-mode vterm vundo whitespace-cleanup-mode with-editor ))
Ensure they're all installed:
(package-install-selected-packages)
No littering!
(require 'no-littering) (setq auto-save-file-name-transforms `((".*" ,(no-littering-expand-var-file-name "auto-save/") t))) (setq custom-file (no-littering-expand-etc-file-name "custom.el")) (load custom-file 'noerror 'nomessage) (when (fboundp 'startup-redirect-eln-cache) (startup-redirect-eln-cache (convert-standard-filename (expand-file-name "var/eln-cache/" user-emacs-directory))))
Add local Elisp directory to load-path
(add-to-list 'load-path (expand-file-name "lisp/" user-emacs-directory))
Display settings
(add-to-list 'initial-frame-alist '(fullscreen . fullboth)) (set-face-attribute 'default nil :height 130 :family "Iosevka") (set-face-attribute 'fixed-pitch nil :family "Iosevka") (set-face-attribute 'variable-pitch nil :family "Iosevka Etoile") (setq ef-themes-mixed-fonts t ef-themes-variable-pitch-ui t ef-themes-to-toggle '(ef-bio ef-day)) (mapc #'disable-theme custom-enabled-themes) (load-theme 'ef-bio :no-confirm) (tool-bar-mode -1) (set-scroll-bar-mode nil) (setq use-dialog-box nil) (setq inhibit-startup-screen t) (setq initial-scratch-message ";; Go.\n") (setq ring-bell-function 'ignore) (setq switch-to-buffer-obey-display-actions t) (setq-default indent-tabs-mode nil) (context-menu-mode) (pixel-scroll-precision-mode) (global-diff-hl-mode) (transient-mark-mode) (mouse-avoidance-mode 'banish) (show-paren-mode) (global-hl-line-mode) (require 'lin) (add-to-list 'lin-mode-hooks 'gnus-summary-mode-hook) (add-to-list 'lin-mode-hooks 'gnus-group-mode-hook) (add-to-list 'lin-mode-hooks 'gnus-server-mode-hook) (lin-global-mode 1) (add-hook 'completion-list-mode-hook (lambda () (setq-local cursor-in-non-selected-windows nil)))
History
(recentf-mode 1) (save-place-mode 1) (setq bookmark-save-flag 1) (savehist-mode 1) (with-eval-after-load 'log-edit (add-to-list 'savehist-additional-variables 'log-edit-comment-ring))
Org-mode settings
Literate config bootstrap
(defconst esy/source-path (locate-user-emacs-file "esy.org") "Path to the Org version of this file.") (defconst esy/target-path (locate-user-emacs-file "esy.el") "Path to the Elisp version of this file.") (defun esy/tangle-and-compile-config () "Tangle literate configuration file." (interactive) (when (file-newer-than-file-p esy/source-path esy/target-path) (require 'org) (require 'ob) (org-babel-tangle-file esy/source-path esy/target-path (rx string-start (or "emacs-lisp" "elisp") string-end)) (byte-compile-file esy/target-path))) (add-hook 'kill-emacs-hook #'esy/tangle-and-compile-config) (defun esy/find-esy-org () "Open my Emacs configuration." (interactive) (find-file esy/source-path))
Org-mode basic settings
(defconst esy/inbox-path (expand-file-name "inbox.org" "~/org") "Path to my Org mode inbox file.") (defconst esy/journal-path (expand-file-name "journal.org" "~/org") "Path to my Org mode journal file.") (with-eval-after-load 'org (require 'ob) (require 'ob-prolog) (require 'ob-sql) (require 'org-tempo) (setq org-agenda-files `(,esy/inbox-path ,esy/journal-path) org-default-notes-file esy/inbox-path org-agenda-start-on-weekday 0 org-ellipsis "…" org-todo-keywords '((sequence "TODO(t)" "BLOCKED([email protected]/!)" "INPROGRESS(i!)" "|" "DONE(d!)" "CANCELED([email protected])")) org-babel-load-languages '((emacs-lisp . t) (shell . t) (sql . t) (bnf . t) (prolog . t)) org-confirm-babel-evaluate nil org-log-done 'time org-log-into-drawer t org-use-fast-todo-selection 'expert org-clock-in-switch-to-state "INPROGRESS") (add-to-list 'org-src-lang-modes '("prolog" . sweeprolog)) (keymap-unset org-mode-map "C-," t)) (with-eval-after-load 'org-agenda (add-to-list 'org-agenda-custom-commands '("w" "Work TODOs" tags-todo "+work")))
Always open files with C-c C-o
inside Emacs
(setq org-file-apps '((t . emacs)))
Refile targets
By default, org-refile
considers only top level heading to be
candidates for refiling into, we set org-refile-targets
to allow
refiling directly into deeper headings as well.
(with-eval-after-load 'org-refile (setq org-refile-targets '((org-agenda-files . (:maxlevel . 5)) (nil . (:maxlevel . 3))) org-archive-location "~/org/journal.org::datetree/* Finished Tasks :ARCHIVE" org-refile-use-outline-path t))
Interactively fill missing CUSTOM_ID properties cmd
(defun esy/org-fill-custom-id (point value) "Set CUSTOM_ID to VALUE interactively for the entry at POINT." (interactive "d\nMCUSTOM_ID: ") (org-entry-put point "CUSTOM_ID" value)) (defun esy/org-fill-description (point value) "Set DESCRIPTION to VALUE interactively for the entry at POINT." (interactive "d\nMDESCRIPTION: ") (org-entry-put point "DESCRIPTION" value)) (defun esy/org-maybe-prompt-custom-id () "Prompt for CUSTOM_ID if not set for the entry at POINT." (let ((res 0)) (unless (and (org-entry-get (point) "DESCRIPTION") (org-entry-get (point) "CUSTOM_ID")) (pulse-momentary-highlight-one-line) (org-cycle) (unless (org-entry-get (point) "CUSTOM_ID") (call-interactively #'esy/org-fill-custom-id) (setq res (1+ res))) (unless (org-entry-get (point) "DESCRIPTION") (call-interactively #'esy/org-fill-description) (setq res (1+ res))) (org-global-cycle 1)) res)) (defun esy/org-fill-custom-ids-in-buffer () "Visit headers in the current buffer and set CUSTOM_ID for each." (interactive) (org-global-cycle 1) (message "Filled %d properties." (apply #'+ (remove nil (org-map-entries #'esy/org-maybe-prompt-custom-id)))))
Org-mode capture templates
(defun esy/org-capture-to-project-heading () "Prompt for a projects and capture a related task." (let* ((projects (org-map-entries `(lambda () (nth 4 (org-heading-components))) "+project+LEVEL=2" `(,esy/inbox-path))) (choice (completing-read "Project: " projects nil t nil)) (m (org-find-olp (cons (org-capture-expand-file esy/inbox-path) (list "Projects" choice))))) (set-buffer (marker-buffer m)) (org-capture-put-target-region-and-position) (widen) (goto-char m) (set-marker m nil))) (defun esy/org-capture-to-current-project () "Prompt for a projects and capture a related task." (let* ((projects (org-map-entries (lambda () (nth 4 (org-heading-components))) (concat "+project+LEVEL=2+SCM=\"file:" (project-root (with-current-buffer (org-capture-get :original-buffer) (project-current))) "\"") (list esy/inbox-path))) (choice (car projects)) (m (org-find-olp (cons (org-capture-expand-file esy/inbox-path) (list "Projects" choice))))) (set-buffer (marker-buffer m)) (org-capture-put-target-region-and-position) (widen) (goto-char m) (set-marker m nil))) (setq org-capture-templates '(("t" "Todo [inbox]" entry (file+headline esy/inbox-path "Tasks") "** TODO %^{Task} %^g :PROPERTIES: :CreatedAt: %t :CapturedAt: %a :CapturedAs: Inbox Task :END:" :prepend t :empty-lines 1 :immediate-finish t) ("w" "Work [inbox]" entry (file+headline esy/inbox-path "Tasks") "** TODO %^{Task} :work: :PROPERTIES: :CreatedAt: %t :CapturedAt: %a :CapturedAs: Work Task :END:" :prepend t :empty-lines 1 :immediate-finish t) ("e" "Emacs configuration fragment" entry (file+headline esy/source-path "Misc. settings") "** %^{Fragment} %^g :PROPERTIES: :CUSTOM_ID: %^{CUSTOM_ID} :CreatedAt: %t :CapturedAt: %a :CapturedAs: Emacs configuration fragment :END:\n\n#+begin_src emacs-lisp\n %i\n#+end_src" :empty-lines 1) ("c" "New Calendar Event" entry (file+headline esy/inbox-path "Calendar") "** %^{Title} %^g :PROPERTIES: :CreatedAt: %t :CapturedAt: %a :CapturedAs: Calendar Event :END: %(format-time-string \"<%Y-%m-%d %H:%M\" (org-read-date t t))-%(format-time-string \"%H:%M>\" (org-read-date t t)) %i" :prepend t :empty-lines 1 :immediate-finish t) ("j" "Journal" entry (file+datetree esy/journal-path) "* %? :PROPERTIES: :CreatedAt: %t :CapturedAt: %a :CaptuerdAs: Journal entry :END: %i" :empty-lines 1))) (setq org-capture-templates-contexts '(("P" (list project-current))))
Open files (with C-c C-o
) in Emacs
(setq org-file-apps '((t . emacs)))
Email settings
My accounts
(setq user-full-name "Eshel Yaron") (setq user-mail-address "[email protected]") (defconst esy/user-mail-address-gmail "[email protected]" "My personal Gmail address.") (defconst esy/user-mail-address-swipl "[email protected]" "My SWI-Prolog email address.") (defconst esy/user-mail-address-dazz "[email protected]" "My Dazz email address.") (defconst esy/user-mail-address-me "[email protected]" "My persomal email address.")
Sending mail from multiple SMTP accounts
(defun esy/smtpmail-multi-make-accout (address server) "Return an SMTP account definition for ADDRESS in SERVER." `(,address ,server 587 ,address starttls nil nil nil)) (defun esy/smtpmail-multi-make-rx (address) "Return a regexp that matches ADDRESS wrapped with anything." (rx (* anything) (literal address) (* anything))) (defun esy/customize-message-mode () "Configure `message-mode' specific customizations." (require 'smtpmail-multi) (setq smtpmail-multi-accounts `((daz . ,(esy/smtpmail-multi-make-accout esy/user-mail-address-dazz "smtp.gmail.com")) (esy . ,(esy/smtpmail-multi-make-accout esy/user-mail-address-gmail "smtp.gmail.com")) (swp . ,(esy/smtpmail-multi-make-accout esy/user-mail-address-swipl "mail.swi-prolog.com")) (me . ,(esy/smtpmail-multi-make-accout esy/user-mail-address-me "mail.eshelyaron.com")))) (setq smtpmail-multi-associations `((,(esy/smtpmail-multi-make-rx esy/user-mail-address-dazz) daz) (,(esy/smtpmail-multi-make-rx esy/user-mail-address-gmail) esy) (,(esy/smtpmail-multi-make-rx esy/user-mail-address-swipl) swp) (,(esy/smtpmail-multi-make-rx esy/user-mail-address-me) me))) (setq send-mail-function #'smtpmail-multi-send-it) (setq message-send-mail-function #'smtpmail-multi-send-it)) (add-hook 'message-mode-hook #'esy/customize-message-mode) (add-hook 'mail-mode-hook #'esy/customize-message-mode)
Reading mail with Gnus
(setq mail-user-agent 'gnus-user-agent gnus-always-read-dribble-file t gnus-expert-user t gnus-inhibit-startup-message t gnus-select-method '(nnimap "gmail" (nnimap-address "imap.gmail.com") (nnimap-server-port "imaps") (nnimap-stream ssl)) gnus-secondary-servers '((nnimap "me" (nnimap-address "mail.eshelyaron.com") (nnimap-server-port "imaps") (nnimap-stream ssl) (nnimap-authinfo-file "~/.authinfo")))) (defun esy/customize-gnus-mode () "Configure Gnus specific customizations." (require 'gnus) (require 'gnus-icalendar) (setq gnus-use-full-window nil gnus-article-treat-types '("text/plain" "text/x-verbatim" "text/x-patch" "text/html" "text/calendar") gnus-posting-styles `((".*eshelyaron.com.*" (address ,esy/user-mail-address-me)) (".*mail.swi-prolog.com.*" (address ,esy/user-mail-address-swipl)) (".*" (address ,esy/user-mail-address-gmail))) gnus-icalendar-org-capture-file esy/inbox-path gnus-icalendar-org-capture-headline '("Calendar") calendar-date-style 'iso) (gnus-icalendar-setup) (gnus-icalendar-org-setup) (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)) (add-hook 'gnus-mode-hook #'esy/customize-gnus-mode) (with-eval-after-load 'gnus (require 'all-the-icons-gnus) (all-the-icons-gnus-setup))
Global keybindings kbd
C-c
keybindings
(keymap-global-set "C-c w" #'esy/eww) (keymap-global-set "C-c c" #'org-capture) (keymap-global-set "C-c l" #'org-store-link) (keymap-global-set "C-c a" #'org-agenda) (keymap-global-set "C-c !" #'consult-flymake) (keymap-global-set "C-c E" #'elfeed) (keymap-global-set "C-c G" #'gnus) (keymap-global-set "C-c M" #'mastodon) (keymap-global-set "C-c S" #'esy/vterm-at) (keymap-global-set "C-c V" #'vertalen-at-point) (keymap-global-set "C-c F" #'esy/find-esy-org)
Custom kill command cmd
(defun esy/kill-dwim () "When region is active, kill region, otherwise kill last word." (interactive) (if (region-active-p) (let ((beg (region-beginning)) (end (region-end))) (kill-region beg end)) (let ((end (point))) (backward-word) (kill-region (point) end))))
Pulse current line cmd
(defun esy/pulse-line (&optional _) "Pulse current line." (interactive) (pulse-momentary-highlight-one-line))
Misc. keybindings
(keymap-global-unset "s-a") (keymap-global-unset "s-d") (keymap-global-unset "s-e") (keymap-global-unset "s-f") (keymap-global-unset "s-g") (keymap-global-unset "s-h") (keymap-global-unset "s-j") (keymap-global-unset "s-k") (keymap-global-unset "s-l") (keymap-global-unset "s-m") (keymap-global-unset "s-o") (keymap-global-unset "s-q") (keymap-global-unset "s-s") (keymap-global-unset "s-t") (keymap-global-unset "s-u") (keymap-global-unset "s-w") (keymap-global-unset "s-x") (keymap-global-unset "s-y") (keymap-global-unset "s-z") (global-set-key [remap kill-region] #'esy/kill-dwim) (global-set-key [remap goto-line] #'consult-goto-line) (global-set-key [remap suspend-frame] #'zap-up-to-char) (global-set-key [remap imenu] #'consult-imenu) (global-set-key [remap make-frame] #'move-dup-duplicate-down) (global-set-key [remap ns-print-buffer] #'move-dup-duplicate-up) (global-set-key (kbd "M-#") #'define-word-at-point) (global-set-key (kbd "M-o") #'previous-buffer) (global-set-key (kbd "M-O") #'next-buffer) (global-set-key (kbd "C-,") #'backward-delete-char) (global-set-key (kbd "C-.") #'embark-act) (global-set-key (kbd "C-;") #'avy-goto-char-timer) (global-set-key (kbd "C-s-p") #'esy/present-buffer) (global-set-key (kbd "C-s-f") #'toggle-frame-fullscreen) (global-set-key (kbd "C-s-l") #'esy/pulse-line)
C-x
keybindings
(keymap-set ctl-x-map "b" #'consult-buffer) (keymap-set ctl-x-4-map "b" #'consult-buffer-other-window) (keymap-set ctl-x-map "C-b" #'ibuffer) (put 'set-goal-column 'disabled nil) (put 'narrow-to-region 'disabled nil) (put 'narrow-to-page 'disabled nil) (put 'suspend-frame 'disabled t)
M-s
keybindings
(keymap-set search-map "r" #'rg) (keymap-set search-map "l" #'rg-literal) (keymap-set search-map "b" #'some-button)
Applications
Magit
(with-eval-after-load 'magit (setq magit-repository-directories '(("~/checkouts/" . 1))))
Mastodon
(with-eval-after-load 'mastodon (setq mastodon-instance-url "https://emacs.ch" mastodon-active-user "eshel"))
tramp
(with-eval-after-load 'tramp (tramp-set-completion-function "ssh" '((tramp-parse-netrc "~/.authinfo")))) (define-advice project--find-in-directory (:override (dir) no-remote-projects) (unless (file-remote-p dir) (run-hook-with-args-until-success 'project-find-functions dir))) (define-advice completion-file-name-table (:filter-args (args) no-remote-file-name-completion) (let ((string (car args)) (pred (cadr args)) (action (caddr args))) (if (and (file-remote-p string) (eq pred #'file-exists-p)) (list string nil action) (list string pred action))))
Dired
(defun esy/local-all-the-icons-dired-mode () (unless (file-remote-p default-directory) (all-the-icons-dired-mode))) (with-eval-after-load 'dired (put 'dired-find-alternate-file 'disabled nil) (setq dired-dwim-target t) (add-hook 'dired-mode-hook #'esy/local-all-the-icons-dired-mode))
vterm cmd
Start vterm
in a given directory and with a given shell, possibly
over ssh for remote connections.
(defun esy/vterm-mode-hook-function () (setq-local term-prompt-regexp "^[^#$%>\n]*[#$%>] *" global-hl-line-mode nil)) (defun esy/vterm-with (arg shell) (interactive (list current-prefix-arg (completing-read "Start vterm with shell: " '("bash" "zsh" "sh") nil t nil nil "bash"))) (require 'vterm) (let ((vterm-shell (concat "/bin/" shell))) (vterm arg))) (defun esy/vterm-at (arg dir) (interactive (list current-prefix-arg (read-directory-name "Start vterm in directory: " ))) (require 'vterm) (let ((default-directory dir)) (vterm arg))) (with-eval-after-load 'vterm (setq vterm-shell "/bin/zsh" vterm-max-scrollback 2048 vterm-kill-buffer-on-exit nil vterm-use-vterm-prompt-detection-method t) (add-to-list 'vterm-tramp-shells '("kubernetes" "/bin/bash")) (add-hook 'vterm-mode-hook #'esy/vterm-mode-hook-function))
Elfeed
(with-eval-after-load 'elfeed (setq elfeed-feeds '( "https://ajroach42.com/feed.xml" "https://emacs.dyerdwelling.family/index.xml" "https://www.typetheoryforall.com/episodes.mp3.rss" "https://www.fsf.org/static/fsforg/rss/news.xml" "https://amodernist.com/all.atom" "https://arcology.garden/updates.xml" "https://takeonrules.com/index.atom" "https://atthis.link/rss.xml" "https://archive.casouri.cc/note/atom.xml" "https://cestlaz.github.io/rss.xml" "https://drewdevault.com/blog/index.xml" "https://herman.bearblog.dev/feed/" "https://lwn.net/headlines/rss" "https://maggieappleton.com/rss.xml" "https://matt-rickard.com/rss" "https://node2.feed43.com/7487052648530856.xml" "https://njoseph.me/shaarli/feed/atom?" "https://nullprogram.com/feed/" "https://olddeuteronomy.github.io/index.xml" "https://parasurv.neocities.org/rss.xml" "https://phaazon.net/blog/feed" "https://planet.emacslife.com/atom.xml" "https://pouria.dev/rss.xml" "https://project-mage.org/rss.xml" "https://reddit.com/r/prolog/.rss" "https://sachachua.com/blog/feed/" "https://stephanango.com/feed.xml" "https://stppodcast.libsyn.com/rss" "https://writer13.neocities.org/rss.xml" "https://www.draketo.de/rss-feed.xml" "https://www.haskellforall.com/feeds/posts/default" "https://cce.whatthefuck.computer/updates.xml" "https://xkcd.com/rss.xml" "https://bitspook.in/blog/feed.xml" "https://flower.codes/feed.xml" )))
eww www
(with-eval-after-load 'eww (setq eww-auto-rename-buffer 'title browse-url-browser-function #'eww-browse-url)) (with-eval-after-load 'shr (setq shr-use-colors nil))
Prompt for URL with history-based completion
The eww
command in the heart of eww.el
prompts the user for a URL,
and browses it (see eww#Basics for more details). One shortcoming of
the built-in eww
command is that it uses the read-string
function
to read the requested URL, which does not facilitate completions.
The following fragment, inspired by Protesilaos Stavrou's prot-eww.el,
provides a simple wrapper for eww
that uses completing-read
instead of read-string
, allowing for quick input completion based on
the prior submitted URLs and web search keywords.
(defun esy/eww () "Prompt for a URL or keywords to search the web for." (interactive) (require 'eww) (eww (mapconcat #'identity (completing-read-multiple "Browse or search: " eww-prompt-history nil nil nil 'eww-prompt-history (car (eww-suggested-uris))) " ")))
Use eww
as the default web browser
(with-eval-after-load 'browse-url (setq browse-url-browser-function #'eww-browse-url))
Proced
proced.el
is an Elisp library built into Emacs that provides a
listing of the currently running system processes. The following code
fragment hooks proced
to set proced-auto-update-flag
variable to
t
on startup, making M-x proced
behave similarly to how top(1)
does in the shell.
(defun esy/setup-proced () "Setup `proced-mode' specific settings." (setq proced-auto-update-flag t)) (add-hook 'proced-mode-hook #'esy/setup-proced)
BBDB
(with-eval-after-load 'bbdb (setq bbdb-phone-style nil)) (with-eval-after-load 'gnus (bbdb-initialize 'gnus 'mail 'message))
Slack
(defun esy/slack-start () (interactive) (require 'slack) (require 'auth-source) (slack-register-team :name "Dazz" :default t :token (auth-source-pick-first-password :host "dazz-io.slack.com" :user "eshel") :cookie (auth-source-pick-first-password :host "dazz-io.slack.com" :user "eshel^cookie")) (setq slack-prefer-current-team t slack-buffer-emojify t) (slack-start))
EMMS
(emms-minimalistic) (setq emms-player-list '(emms-player-mpv))
Dutch to English translation with define-word
and vertalen.nu
(defun vertalen--on-success (&rest args) "Process ARGS and display translation in a dedicated buffer." (with-current-buffer-window "*vertalen*" (with-selected-window (selected-window) (unless (eq major-mode 'vertalen-mode) `(nil . ((inhibit-same-window . t))))) #'fit-window-to-buffer (setq tabulated-list-entries (plist-get args :data)) (vertalen-mode) (+ 2 (length tabulated-list-entries)))) (defun vertalen--parse () (require 'dom) "Parse buffer and return a list of translations." (let* ((dom (libxml-parse-html-region (point-min) (point-max))) (res (dom-by-class dom ".*result-item-translations.*")) (ret nil)) (dolist (elem res) (setq ret `((nil ,(vector (apply #'concat (dom-strings (car (dom-by-class elem ".*result-item-source.*")))) (mapconcat #'identity (dom-strings (car (dom-by-class elem ".*result-item-target.*"))) ", "))) . ,ret))) (reverse ret))) (setq vertalen--source nil) (setq vertalen-history nil) (with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'vertalen-history)) (defun vertalen (word) "Translate WORD." (interactive (list (read-string "Translate: " nil 'vertalen-history (thing-at-point 'word)))) (require 'request) (setq vertalen--source word) (request "https://www.vertalen.nu/vertaal" :params `(("van" . "nl") ("naar" . "en") ("vertaal" . ,word)) :parser #'vertalen--parse :success #'vertalen--on-success)) (defun vertalen-at-point () "Translate word at point." (interactive nil vertalen-mode) (vertalen (thing-at-point 'word t))) (defvar-keymap vertalen-mode-map :doc "Keymap for `vertalen-mode' buffers." "RET" #'vertalen-at-point) (define-derived-mode vertalen-mode tabulated-list-mode "Vertalen" "Major mode for listing Dutch to English translations." (setq tabulated-list-format [("Source Language" 64 t) ("Target Language" 32 t)]) (tabulated-list-init-header) (tabulated-list-print) (save-excursion (goto-char (point-min)) (let ((inhibit-read-only t) (word (search-forward vertalen--source nil t))) (while word (add-face-text-property (match-beginning 0) (match-end 0) 'success) (setq word (search-forward vertalen--source nil t))))))
Minibuffer and completions
Enable and indicate recursive minibuffers
(setq enable-recursive-minibuffers t) (minibuffer-depth-indicate-mode)
Completions
Completion at point
(defun esy/dabbrev-capf () "Workaround for issue with `dabbrev-capf'." (require 'dabbrev) (dabbrev--reset-global-variables) (setq dabbrev-case-fold-search nil) (dabbrev-capf)) (defun esy/file-capf () "File completion at point function." (let ((bs (bounds-of-thing-at-point 'filename))) (when bs (let* ((start (car bs)) (end (cdr bs))) `(,start ,end completion--file-name-table . (:exclusive no)))))) (defun esy/margin-formatter (metadata) "Margin formatter for `corfu-margin-formatters'." (pcase (cdr (assoc 'category metadata)) ('file (lambda (string) (concat (if (string-suffix-p "/" string) (all-the-icons-icon-for-dir string) (all-the-icons-icon-for-file string)) " "))) ('dabbrev (lambda (_) "… ")))) (setq corfu-cycle t corfu-margin-formatters '(esy/margin-formatter) corfu-indexed-start 1) (global-corfu-mode) (corfu-indexed-mode 1) (add-to-list 'completion-at-point-functions #'esy/dabbrev-capf) (add-to-list 'completion-at-point-functions #'esy/file-capf)
Minibuffer-based completions
(setq read-extended-command-predicate #'command-completion-default-include-p completions-format 'one-column completion-auto-select nil completions-detailed nil completion-styles '(orderless partial-completion basic) completion-show-help nil completions-header-format (propertize "%s candidates:\n" 'face 'shadow) completion-auto-help 'visual completions-max-height 16 completion-auto-wrap t) (define-key minibuffer-local-completion-map [remap previous-line] #'minibuffer-previous-completion) (define-key minibuffer-local-completion-map [remap next-line] #'minibuffer-next-completion) (add-hook 'marginalia-mode-hook #'all-the-icons-completion-marginalia-setup) (marginalia-mode) (add-to-list 'display-buffer-alist '("\\*Completions\\*" (display-buffer-reuse-window display-buffer-at-bottom) (window-parameters . ((mode-line-format . none))))) (add-to-list 'all-the-icons-extension-icon-alist '("pl" all-the-icons-alltheicon "prolog" :height 1.1 :face all-the-icons-lmaroon))
Mode-line customizations
(setq display-time-mail-function (lambda () nil)) (display-time-mode) (column-number-mode) (display-battery-mode)
Text mode and derivatives
(add-hook 'text-mode-hook #'flyspell-mode) (with-eval-after-load 'flyspell (keymap-unset flyspell-mode-map "C-," t) (keymap-unset flyspell-mode-map "C-." t) (keymap-unset flyspell-mode-map "C-;" t))
LaTeX and PDF settings
(pdf-tools-install t) (add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode) (setq TeX-view-program-selection '((output-pdf "PDF Tools")) TeX-source-correlate-start-server t) (add-to-list 'revert-without-query "\\.pdf\\'") (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer) (add-to-list 'auto-mode-alist '("\\.pdf\\'" . pdf-view-mode))
Programming
Compilation
(setq safe-local-variable-values '((compilation-read-command . nil)))
General prog-mode
settings
(defun esy/setup-programming () "Setup `prog-mode' and more programming-related settings." (require 'rainbow-delimiters) (require 'flymake) (rainbow-delimiters-mode) (display-line-numbers-mode) (display-fill-column-indicator-mode) (flymake-mode)) (add-hook 'prog-mode-hook #'esy/setup-programming)
Lisp specific settings
Paredit
Enable paredit-mode
in lisp-data-mode
and its derivatites, which
include emacs-lisp-mode
and lisp-interaction-mode
.
(defun esy/setup-lisp () "Setup Lisp specific settings." (require 'paredit) (enable-paredit-mode)) (add-hook 'lisp-data-mode-hook #'esy/setup-lisp)
Haskell specific settings
(add-hook 'haskell-mode-hook #'interactive-haskell-mode) (add-hook 'haskell-mode-hook #'haskell-decl-scan-mode) (add-hook 'haskell-mode-hook #'haskell-doc-mode)
Prolog specific settings
Setup sweep
(require 'use-package) (use-package sweeprolog :mode ("\\.plt?\\'" . sweeprolog-mode) :bind-keymap ("C-c p" . sweeprolog-prefix-map) :config (add-hook 'sweeprolog-mode-hook #'sweeprolog-electric-layout-mode) (add-hook 'sweeprolog-top-level-mode-hook #'compilation-shell-minor-mode))
Make rg
regard .pl
files as Prolog rather than Perl
(with-eval-after-load 'rg (add-to-list 'rg-custom-type-aliases '("Prolog" . "*.pl *.plt *.pro *.prolog")))
Rust
(add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-ts-mode))
Terraform
(use-package terraform-mode :config (setq terraform--resource-name-face font-lock-function-name-face terraform--resource-type-face font-lock-type-face))
Dockerfile
(add-to-list 'auto-mode-alist '("Dockerfile" . dockerfile-ts-mode))
vc
customizations
By default, Emacs asks for confirmation when visiting a path that
points to symlink to a file committed to version control. I often
visit this file with C-x C-f ~/.emacs.d/esy.org
, which is a symlink
to the actual source of this file which is managed by git and Stow, so
in this cases I prefer Emacs to trust me that I know what I'm doing
without asking each time.
(setq vc-follow-symlinks t)
Project management
Populate project list from my projects directory
(defconst esy/projects-directory "~/checkouts/" "Path of the projects directory.") (add-hook 'kill-emacs-hook (lambda () (require 'project) (mapcar #'project-remember-projects-under (directory-files esy/projects-directory))))
Project switch commands
(with-eval-after-load 'project (add-to-list 'project-switch-commands '(project-compile "Compile")) (add-to-list 'project-switch-commands '(rg-project "rg")) (add-to-list 'project-switch-commands '(magit-project-status "Magit")) (add-to-list 'project-switch-commands '(project-shell "Shell")) (when (boundp 'project-prefix-map) (define-key project-prefix-map "R" #'rg-project) (define-key project-prefix-map "m" #'magit-project-status)))
shell
customizations
Kill shell
buffers on exit
Kill M-x shell
buffers automatically when the shell process
terminates, e.g. when pressing C-d
.
(setq shell-kill-buffer-on-exit t)
Restart async shell process cmd kbd
(defun shell-restart-process () "Restart process of `shell-mode' buffer." (interactive) (let* ((proc (get-buffer-process (current-buffer))) (pid (process-id proc)) (cmd (caddr (process-command proc)))) (message "%s" cmd) (delete-process proc) (async-shell-command cmd))) (with-eval-after-load 'shell (keymap-set shell-mode-map "C-c C-k" #'shell-restart-process) (keymap-set shell-mode-map "SPC" #'comint-magic-space))
Save logs of comint
buffers cmd kbd
(defvar esy/logs-directory "~/logs" "Directory where some log files will be saved.") (defvar-local esy/log-buffer-filename nil "File in which the current buffer is logged.") (defun esy/log-buffer () "Save the current buffer under `esy/logs-directory'." (interactive) (let ((filename (or esy/log-buffer-filename (setq esy/log-buffer-filename (expand-file-name (concat mode-name "_" (format-time-string "%Y%m%d%H%M%S" (current-time)) ".log") esy/logs-directory))))) (save-restriction (widen) (write-region (point-min) (point-max) filename)))) (with-eval-after-load 'comint (keymap-set comint-mode-map "C-c C-q" #'esy/log-buffer))
Misc. settings
(setq global-auto-revert-non-file-buffers t auto-revert-verbose nil query-about-changed-file t kill-do-not-save-duplicates t) (global-auto-revert-mode) (global-whitespace-cleanup-mode 1)
Enable embark-consult
This is a little companion library for embark
that makes embark-export
and friends work correctly with consult
enabled completions.
(with-eval-after-load 'consult (with-eval-after-load 'embark (require 'embark-consult)))
Add a repeat-map to tranpose-lines
(defvar-keymap transpose-lines-repeat-map :doc "Repeat map for \\[transpose-lines]" "C-t" #'transpose-lines) (put 'transpose-lines 'repeat-map 'transpose-lines-repeat-map)
Use consult
to show xref
results
(with-eval-after-load 'xref (setq xref-show-definitions-function #'consult-xref xref-show-xrefs-function #'consult-xref xref-search-program 'ripgrep))
Show the time in Amsterdam in world-clock
Add the timezones of places of interest to the list of clocks shown by
M-x world-clock
. The list of named timezones is maintained by IANA.
(with-eval-after-load 'time (add-to-list 'zoneinfo-style-world-list '("Europe/Amsterdam" "Amsterdam")))
Enable repeat-mode
(repeat-mode)
Sibling files
(setq find-sibling-rules '(("\\([^/]+\\)\\.c\\'" "\\1.h")))
Small command for converting Unix timestamps to date strings
(defun esy/seconds-to-date-string (seconds) (interactive (list (if (use-region-p) (string-to-number (buffer-substring (region-beginning) (region-end))) (read-number "Unix timestamp: " (round (float-time)))))) (message (format-time-string "%FT%T%z" (seconds-to-time seconds))))
Predefined SQL connections
(with-eval-after-load 'sql (setq sqlformat-command 'pgformatter) (define-key sql-mode-map (kbd "C-c C-f") 'sqlformat) (setq sql-input-ring-file-name (expand-file-name ".sqli-history" no-littering-var-directory)) (setq sql-connection-alist (let* ((a (auth-source-search :port 5432 :max 5 :require '(:user :port :secret :host))) (d (nth 0 a)) (p (nth 1 a)) (c (nth 2 a)) (e (nth 3 a)) (f (nth 4 a))) `((dev (sql-product 'postgres) (sql-user ,(plist-get d :user)) (sql-port 5432) (sql-password ,(funcall (plist-get d :secret))) (sql-server ,(plist-get d :host)) (sql-database "alerts")) (prod (sql-product 'postgres) (sql-user ,(plist-get p :user)) (sql-port 5432) (sql-password ,(funcall (plist-get p :secret))) (sql-server ,(plist-get p :host)) (sql-database "alerts")) (cgs (sql-product 'postgres) (sql-user ,(plist-get c :user)) (sql-port 5432) (sql-password ,(funcall (plist-get c :secret))) (sql-server ,(plist-get c :host)) (sql-database "container_graph")) (ten (sql-product 'postgres) (sql-user ,(plist-get e :user)) (sql-port 5432) (sql-password ,(funcall (plist-get e :secret))) (sql-server ,(cadr (split-string (plist-get e :host) (rx "^")))) (sql-database ,(car (split-string (plist-get e :host) (rx "^"))))) (ac (sql-product 'postgres) (sql-user ,(plist-get f :user)) (sql-port 5432) (sql-password ,(funcall (plist-get f :secret))) (sql-server ,(cadr (split-string (plist-get f :host) (rx "^")))) (sql-database ,(car (split-string (plist-get f :host) (rx "^")))))))) (define-advice sql-comint-postgres (:around (fun &rest args) use-sql-password) (let ((process-environment (nconc (list (format "PGPASSWORD=%s" sql-password)) process-environment))) (apply fun args))))