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.

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

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

Restart async shell process in process-menu-mode   cmd kbd

(defun process-menu-restart-process ()
  "Restart process at point in a `list-processes' buffer."
  (interactive)
  (let* ((pos (point))
         (pid (tabulated-list-get-id))
         (ent (tabulated-list-get-entry))
         (cmd (combine-and-quote-strings
               (seq-drop (split-string-shell-command (seq-elt ent 6))
                         2))))
    (delete-process pid)
    (async-shell-command cmd)
    (revert-buffer)))

(with-eval-after-load 'simple
  (keymap-set process-menu-mode-map
              "r"
              #'process-menu-restart-process))

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